UNPKG

media-downloader-ez

Version:

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

377 lines (326 loc) 10.9 kB
/** * * Last update : * Added second method to download videos * */ const axios = require('axios'); const fs = require('fs'); const { alldown } = require("nayan-videos-downloader"); // Import btch-downloader as a fallback method const { igdl, ttdl, fbdown, twitter, youtube } = require('btch-downloader'); 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://open.spotify.com", "https://www.facebook.com", "https://facebook.com", "https://www.tiktok.com", "https://tiktok.com", "https://www.x.com", "https://x.com", "https://www.twitter.com", "https://twitter.com", "https://www.instagram.com", "https://instagram.com", "https://www.youtube.com", "https://youtube.com", "https://youtu.be", "https://www.pinterest.com", "https://pinterest.com", "https://drive.google.com", "https://www.google.com/drive", "https://www.capcut.com", "https://capcut.com", "https://www.likee.video", "https://likee.video", "https://www.threads.net", "https://threads.net" ]; // 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)); }; function blacklistLink(link) { const found = linkCant.find(item => link.includes(item.link)); return found; } const defaultConfig = { autocrop: false, limitSizeMB: null, }; // Fonction pour obtenir le type de plateforme à partir de l'URL function getPlatformType(url) { if (url.includes("instagram.com")) return "instagram"; if (url.includes("tiktok.com")) return "tiktok"; if (url.includes("facebook.com")) return "facebook"; if (url.includes("twitter.com") || url.includes("x.com")) return "twitter"; if (url.includes("youtube.com") || url.includes("youtu.be")) return "youtube"; return "unknown"; } // Nouvelle fonction pour essayer la méthode btch-downloader comme fallback async function tryFallbackDownload(url) { const platform = getPlatformType(url); let data; try { switch (platform) { case "instagram": data = await igdl(url); break; case "tiktok": data = await ttdl(url); break; case "facebook": data = await fbdown(url); break; case "twitter": data = await twitter(url); break; case "youtube": data = await youtube(url); break; default: throw new Error("Platform not supported by fallback downloader"); } //console.log(data) // Extract the video URL from the response let videoUrl = null; if (data) { // Handle different response formats for different platforms if (platform === "instagram" && data && data.length > 0) { videoUrl = data[0].url; } else if (platform === "tiktok" && data && data.video && data.video[0]) { videoUrl = data.video[0]; } else if (platform === "twitter" && data && data.url && data.url.length > 0 && data.url[1] && data.url[1].sd) { videoUrl = data.url[1].sd } else if (platform === "youtube" && data.link && data.link.length > 0) { // Get the highest quality video URL data.link.sort((a, b) => (b.size || 0) - (a.size || 0)); videoUrl = data.link[0].url; } } return videoUrl; } catch (error) { console.log(`Fallback download failed for ${platform}: ${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); if (blacklistLink(url)) { let obj = blacklistLink(url); if (obj.reason) { throw new Error("URL not supported. " + obj.reason); } else { throw new Error("URL blacklisted and not supported"); } } if (!isVideoLink(url)) { const videofile = await downloadDirectVideo(url, config); if (videofile) { return videofile; } else { throw new Error("URL not supported. Please provide a video URL from a valid platform."); } } await deleteTempVideos(); if (url.includes("http")) { try { // Try the primary method first const videofile = await downloadSmartVideo(url, config); return 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) { // console.log(`Fallback URL found: ${fallbackUrl}`); return 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..."); } }; async function downloadSmartVideo(url, config) { try { let data = await alldown(url); if (!data || !data.data) { throw new Error("Can't download this link."); } if (data.data.low) { url = data.data.low; } else if (data.data.high) { url = data.data.high; } else { throw new Error("Can't download this link."); } const response = await axios({ url: url, method: 'GET', responseType: 'stream' }); 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 () => { if (config.autocrop) { try { const croppedFileName = await autoCrop(fileName); resolve(await checkAndCompressVideo(croppedFileName, config.limitSizeMB)); } catch (error) { reject(error); } } else { resolve(await checkAndCompressVideo(fileName, config.limitSizeMB)); } }); 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' }); 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 () => { if (config.autocrop) { try { const croppedFileName = await autoCrop(fileName); resolve(await checkAndCompressVideo(croppedFileName, config.limitSizeMB)); } catch (error) { reject(error); } } else { resolve(await checkAndCompressVideo(fileName, config.limitSizeMB)); } }); videoWriter.on('error', (error) => reject(error)); }); } catch (error) { throw new Error(`An error occurred while downloading video: ${error.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('Erreur: Impossible de détecter les valeurs de crop.')); return; } ffmpeg(inputPath) .videoFilters(`crop=${crop.width}:${crop.height}:${crop.x}:${crop.y}`) .on('end', () => { resolve(outputPath); }) .on('error', (err) => { reject(new Error('Erreur lors du rognage: ' + err.message)); }) .save(outputPath); }) .on('error', (err) => { reject(new Error('Erreur lors de la détection du crop: ' + 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('Erreur lors de la compression: ' + err.message)); }) .run(); }); } MediaDownloader.isVideoLink = isVideoLink; module.exports = MediaDownloader;