UNPKG

media-downloader-ez

Version:

download videos from URLs and autocrop (Instagram, YouTube, TikTok, X, etc.) to send directly to Discord or other!

614 lines (539 loc) 19.2 kB
/** * Version: 2.1.4 * Last update: 20/11/2025 * Last update: Added custom API for Instagram download to avoid igdl issues */ const axios = require('axios'); const fs = require('fs'); // Import btch-downloader as the only method const { igdl, ttdl, fbdown, youtube, mediafire, capcut, gdrive, pinterest } = require('btch-downloader'); const { TwitterDL } = require('twitter-downloader'); // Custom Instagram downloader (option3) const instagramCustom = require('./instagramcustom'); // Custom TikTok downloader (twitterpicker) const tiktokCustom = require('./tiktokcustom'); const pathToFfmpeg = require('ffmpeg-ffprobe-static'); const ffmpeg = require('fluent-ffmpeg'); ffmpeg.setFfmpegPath(pathToFfmpeg.ffmpegPath); // Liste des URL des plateformes de vidéos const videoPlatforms = [ "https://www.instagram.com", "https://instagram.com", "https://www.tiktok.com", "https://tiktok.com", "https://www.facebook.com", "https://facebook.com", "https://www.youtube.com", "https://youtube.com", "https://youtu.be", "https://www.mediafire.com", "https://mediafire.com", "https://www.capcut.com", "https://capcut.com", "https://drive.google.com", "https://www.google.com/drive", "https://www.pinterest.com", "https://pinterest.com", "https://x.com", "https://www.x.com", "https://twitter.com", "https://www.twitter.com" ]; // Blacklist links const linkCant = [{ link: "https://vm.tiktok.com", reason: "Use real link like 'https://www.tiktok.com'. (just click on your link and copy the link in the browser)" }]; // Fonction pour vérifier si un lien correspond à une vidéo const isVideoLink = (link) => { return videoPlatforms.some(platform => link.startsWith(platform)); }; // Fonction pour vérifier si un lien est blacklist const blacklistLink = (link) => { return linkCant.find(item => link.includes(item.link)); }; const defaultConfig = { autocrop: false, limitSizeMB: null, rotation: null, // Added rotation parameter YTBmaxduration: 30, // Default duration for YouTube videos useInstaOption3: false // when true force using instagramcustom for Instagram links }; // Fonction pour obtenir le type de plateforme à partir de l'URL function getPlatformType(url) { if (url.includes("instagram.com")) return "instagram"; return "unknown"; } // Nouvelle fonction pour essayer la méthode btch-downloader comme fallback async function tryFallbackDownload(url) { try { // Try igdl first try { const data = await igdl(url); if (data && Array.isArray(data) && data[0] && data[0].url) return data[0].url; } catch (e) { // ignore and try other fallbacks } // If it's a TikTok URL, try twitterpicker-based custom downloader if (url.includes('tiktok.com')) { try { const custom = await tiktokCustom(url); if (typeof custom === 'string' && custom.length) return custom; if (Array.isArray(custom) && custom[0] && custom[0].url) return custom[0].url; if (custom && custom.url) return custom.url; } catch (e) { // ignore } } // If it's an Instagram URL, try instagramCustom if (url.includes('instagram.com')) { try { const custom = await instagramCustom(url); if (typeof custom === 'string') return custom; if (Array.isArray(custom) && custom[0] && custom[0].url) return custom[0].url; if (custom && custom.url) return custom.url; } catch (e) { // ignore } } return null; } catch (error) { console.log(`Fallback download failed: ${error.message}`); return null; } } const MediaDownloader = async (url, options = {}) => { const config = { ...defaultConfig, ...options }; if (!url || !url.includes("http")) { throw new Error("Please specify a video URL..."); } url = extractUrlFromString(url); const blacklisted = blacklistLink(url); if (blacklisted) { throw new Error(`URL not supported. ${blacklisted.reason}`); } if (!isVideoLink(url)) { const videofile = await downloadDirectVideo(url, config); if (videofile) { return getFileName(videofile); } else { throw new Error("URL not supported. Please provide a video URL from a valid platform."); } } await deleteTempVideos(); if (url.includes('youtube') || url.includes('youtu.be')) { if (!options.YTBcookie) { throw new Error("YouTube download requires a cookie. Please provide a valid cookie."); } const videofile = await downloadYoutubeVideo(url, config, options.YTBcookie, options.YTBmaxduration); if (videofile) { return getFileName(videofile); } else { throw new Error("URL not supported. Please provide a video URL from a valid platform."); } } else if (url.includes("http")) { try { // Try the primary method first const videofile = await downloadSmartVideo(url, config, options); return getFileName(videofile); } catch (error) { console.log(`Primary download method failed: ${error.message}`); console.log(`Trying fallback method for ${url}...`); // Try the fallback method if the primary fails const fallbackUrl = await tryFallbackDownload(url); if (fallbackUrl) { return getFileName(await downloadDirectVideo(fallbackUrl, config)); } else { throw new Error(`Failed to download video from ${url} with both methods.`); } } } else { throw new Error("Please specify a video URL from Instagram, YouTube, or TikTok..."); } }; // Ajout du support Twitter via twitter-downloader async function downloadSmartVideo(url, config, options = {}) { try { let videoUrl = null; if (url.includes('instagram.com')) { // If caller forces option3, skip igdl and use instagramCustom directly if (!config.useInstaOption3 && !options.useInstaOption3) { try { const data = await igdl(url); if (data && Array.isArray(data) && data[0] && data[0].url) { videoUrl = data[0].url; } } catch (e) { // igdl failed; we'll attempt instagramCustom below videoUrl = null; } } if (!videoUrl) { // Try the custom instagram downloader try { const custom = await instagramCustom(url); if (typeof custom === 'string' && custom.length) { videoUrl = custom; } else if (Array.isArray(custom) && custom[0] && custom[0].url) { videoUrl = custom[0].url; } else if (custom && custom.url) { videoUrl = custom.url; } } catch (err) { throw new Error("Can't download this link."); } } } else if (url.includes('tiktok.com')) { const data = await ttdl(url); if (!data || !data.video || !data.video[0]) { throw new Error("Can't download this link."); } videoUrl = data.video[0]; } else if (url.includes('facebook.com')) { const data = await fbdown(url); if (!data || !data.links || !data.links[0] || !data.links[0].url) { throw new Error("Can't download this link."); } videoUrl = data.links[0].url; } else if (url.includes('mediafire.com')) { const data = await mediafire(url); if (!data || !data.url) { throw new Error("Can't download this link."); } videoUrl = data.url; } else if (url.includes('capcut.com')) { const data = await capcut(url); if (!data || !data.url) { throw new Error("Can't download this link."); } videoUrl = data.url; } else if (url.includes('drive.google.com') || url.includes('google.com/drive')) { const data = await gdrive(url); if (!data || !data.url) { throw new Error("Can't download this link."); } videoUrl = data.url; } else if (url.includes('pinterest.com')) { const data = await pinterest(url); if (!data || !data.url) { throw new Error("Can't download this link."); } videoUrl = data.url; } else if (url.includes('x.com') || url.includes('twitter.com')) { const data = await TwitterDL(url, {}); if (!data || !data.result || !data.result.media || !data.result.media[0] || !data.result.media[0].videos || !data.result.media[0].videos[0] || !data.result.media[0].videos[0].url) { throw new Error("Can't download this link."); } videoUrl = data.result.media[0].videos[0].url; } else { throw new Error("Platform not supported."); } const response = await axios({ url: videoUrl, method: 'GET', responseType: 'stream', headers: { 'accept': '*/*', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'referer': url } }); let fileName = 'temp_video.mp4'; let count = 1; while (fs.existsSync(fileName)) { fileName = `temp_video_${count}.mp4`; count++; } const videoWriter = fs.createWriteStream(fileName); response.data.pipe(videoWriter); return new Promise((resolve, reject) => { videoWriter.on('finish', async () => { try { let processedFile = fileName; ensureFileNotEmpty(processedFile); if (config.rotation) { processedFile = await rotateVideo(processedFile, config.rotation); ensureFileNotEmpty(processedFile); } if (config.autocrop) { processedFile = await autoCrop(processedFile); ensureFileNotEmpty(processedFile); } processedFile = await checkAndCompressVideo(processedFile, config.limitSizeMB); ensureFileNotEmpty(processedFile); resolve(processedFile); } catch (error) { reject(error); } }); videoWriter.on('error', (error) => reject(error)); }); } catch (error) { throw new Error(`An error occurred while downloading video: ${error.message}`); } } async function downloadDirectVideo(url, config) { try { const response = await axios({ url: url, method: 'GET', responseType: 'stream', headers: { 'accept': '*/*', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'referer': 'https://www.tiktok.com/' } }); let fileName = 'temp_video.mp4'; let count = 1; while (fs.existsSync(fileName)) { fileName = `temp_video_${count}.mp4`; count++; } const videoWriter = fs.createWriteStream(fileName); response.data.pipe(videoWriter); return new Promise((resolve, reject) => { videoWriter.on('finish', async () => { try { let processedFile = fileName; ensureFileNotEmpty(processedFile); // Apply rotation if specified if (config.rotation) { processedFile = await rotateVideo(processedFile, config.rotation); ensureFileNotEmpty(processedFile); } // Apply autocrop if specified if (config.autocrop) { processedFile = await autoCrop(processedFile); ensureFileNotEmpty(processedFile); } // Check and compress if size limit is specified processedFile = await checkAndCompressVideo(processedFile, config.limitSizeMB); ensureFileNotEmpty(processedFile); resolve(processedFile); } catch (error) { reject(error); } }); videoWriter.on('error', (error) => reject(error)); }); } catch (error) { throw new Error(`An error occurred while downloading video: ${error.message}`); } } async function downloadYoutubeVideo(url, config, YTBcookie, YTBmaxduration) { try { const agent = ytdl.createAgent(YTBcookie); const info = await ytdl.getInfo(url, { agent }); const durationSeconds = parseInt(info.videoDetails.lengthSeconds, 10); if (durationSeconds > YTBmaxduration) { throw new Error(`❌ The video is longer than ${YTBmaxduration} seconds. Aborting.`); } let formats = info.formats.filter(format => { return format.contentLength && parseInt(format.contentLength) <= 10 * 1024 * 1024 && // ≤ 10 MB format.hasAudio && format.hasVideo; }); if (formats.length === 0) { formats = info.formats.filter(format => { return format.hasAudio && format.hasVideo; }); if (formats.length === 0) { throw new Error('❌ No format found .'); } } console.log(formats) const bestFormat = formats.sort((a, b) => b.height - a.height)[0]; let fileName = 'temp_video.mp4'; let count = 1; while (fs.existsSync(fileName)) { fileName = `temp_video_${count}.mp4`; count++; } const videoStream = ytdl(url, { format: bestFormat, agent }); const videoWriter = fs.createWriteStream(fileName); videoStream.pipe(videoWriter); return new Promise((resolve, reject) => { videoWriter.on('finish', async () => { try { let processedFile = fileName; ensureFileNotEmpty(processedFile); if (config.rotation) { processedFile = await rotateVideo(processedFile, config.rotation); ensureFileNotEmpty(processedFile); } if (config.autocrop) { processedFile = await autoCrop(processedFile); ensureFileNotEmpty(processedFile); } processedFile = await checkAndCompressVideo(processedFile, config.limitSizeMB); ensureFileNotEmpty(processedFile); resolve(processedFile); } catch (error) { reject(error); } }); videoWriter.on('error', (error) => reject(error)); }); } catch (error) { throw new Error(`An error occurred while downloading the YouTube video: ${error.message}`); } } // New function to handle video rotation async function rotateVideo(fileName, rotation) { const outputPath = fileName.split('.')[0] + "_rotated.mp4"; // Determine rotation angle let angle; switch (rotation.toLowerCase()) { case "left": angle = "90"; break; case "right": angle = "270"; break; case "180": case "flip": angle = "180"; break; default: throw new Error("Invalid rotation value. Use 'left', 'right', '180', or 'flip'"); } return new Promise((resolve, reject) => { ffmpeg(fileName) .videoFilters(`transpose=${angle === "90" ? 2 : angle === "270" ? 1 : 0}${angle === "180" ? ",hflip,vflip" : ""}`) .output(outputPath) .on('end', () => { // Delete the original file since we now have the rotated version fs.unlinkSync(fileName); resolve(outputPath); }) .on('error', (err) => { reject(new Error(`Error during video rotation: ${err.message}`)); }) .run(); }); } function ensureFileNotEmpty(filePath) { try { if (!filePath || typeof filePath !== 'string') { throw new Error('Invalid file path'); } if (!fs.existsSync(filePath)) { throw new Error('File does not exist'); } const stats = fs.statSync(filePath); if (!stats || typeof stats.size !== 'number' || stats.size <= 0) { try { fs.unlinkSync(filePath); } catch (e) { // ignore } throw new Error('Downloaded file is empty (0 bytes)'); } } catch (err) { // Re-throw as normal Error throw new Error(err.message); } } function extractUrlFromString(text) { const urlRegex = /(https?:\/\/[^\s]+)/; const match = text.match(urlRegex); if (match) { return match[0]; } else { return null; } } async function deleteTempVideos() { try { const files = fs.readdirSync("./"); const tempVideoFiles = files.filter(file => file.startsWith('temp_video')); for (const file of tempVideoFiles) { fs.unlinkSync("./" + file); } } catch (error) { throw new Error(`Error deleting temp_video files: ${error.message}`); } } async function autoCrop(fileName) { const inputPath = fileName; const outputPath = fileName.split('.')[0] + "_cropped.mp4"; return new Promise((resolve, reject) => { ffmpeg(inputPath) .videoFilters('cropdetect') .output(outputPath) .on('end', function (stdout, stderr) { const crop = parseCrop(stderr); if (!crop) { reject(new Error('Error: Unable to detect crop values.')); return; } ffmpeg(inputPath) .videoFilters(`crop=${crop.width}:${crop.height}:${crop.x}:${crop.y}`) .on('end', () => { // Delete the original file since we now have the cropped version fs.unlinkSync(inputPath); resolve(outputPath); }) .on('error', (err) => { reject(new Error('Error during cropping: ' + err.message)); } ).save(outputPath); }) .on('error', (err) => { reject(new Error('Error during crop detection: ' + err.message)); }) .run(); }); function parseCrop(stderr) { const cropRegex = /crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/; const match = stderr.match(cropRegex); if (match) { return { width: match[1], height: match[2], x: match[3], y: match[4], }; } else { return null; } } } async function checkAndCompressVideo(filePath, limitSizeMB) { if (!limitSizeMB) return filePath; const stats = fs.statSync(filePath); const fileSizeInMB = stats.size / (1024 * 1024); if (fileSizeInMB <= limitSizeMB) { return filePath; } const compressedFilePath = filePath.split('.')[0] + "_compressed.mp4"; return new Promise((resolve, reject) => { ffmpeg(filePath) .outputOptions([ '-vf', 'scale=640:-2', '-b:v', '500k', '-b:a', '128k', '-movflags', 'faststart' ]) .output(compressedFilePath) .on('end', () => { fs.unlinkSync(filePath); resolve(compressedFilePath); }) .on('error', (err) => { reject(new Error('Error during compression: ' + err.message)); }) .run(); }); } function getFileName(filePath) { return filePath.split('/').pop(); } MediaDownloader.isVideoLink = isVideoLink; module.exports = MediaDownloader;