diff --git a/.gitignore b/.gitignore index e47c263..633f72b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ node_modules __pycache__ props !props/resources.json -*.mp4 \ No newline at end of file +*.mp4 +plugins/yt-dlp +tmp \ No newline at end of file diff --git a/bot.js b/bot.js index eec5e91..1f07377 100644 --- a/bot.js +++ b/bot.js @@ -3,6 +3,7 @@ const path = require('path'); const fs = require('fs'); const Config = require('./props/config.json'); const { isOnSpamWatch } = require('./plugins/lib-spamwatch/spamwatch.js'); +require('./plugins/ytdlp-wrapper.js'); // require('./plugins/termlogger.js'); const bot = new Telegraf(Config.botToken); diff --git a/commands/youtube.js b/commands/youtube.js index 3bcaf70..87d5278 100644 --- a/commands/youtube.js +++ b/commands/youtube.js @@ -1,33 +1,88 @@ -var exec = require('child_process').exec; +const { getStrings } = require('../plugins/checklang.js'); +const { isOnSpamWatch } = require('../plugins/lib-spamwatch/spamwatch.js'); +const spamwatchMiddleware = require('../plugins/lib-spamwatch/Middleware.js')(isOnSpamWatch); +const { exec } = require('child_process'); +const os = require('os'); const fs = require('fs'); -const { getStrings } = require('../plugins/checklang'); +const path = require('path'); -async function DownloadFromYoutube(command) { - return new Promise((resolve, reject) => { +const ytDlpPaths = { + linux: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp'), + win32: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp.exe'), + darwin: path.resolve(__dirname, '../plugins/yt-dlp/yt-dlp_macos'), +}; - exec(command, (error, stdout, stderr) => { - if (error) { - reject({ error, stdout, stderr }); - } else { - resolve({ error, stdout, stderr }); - } - }); +function getYtDlpPath() { + const platform = os.platform(); + return ytDlpPaths[platform] || ytDlpPaths.linux; +}; + +async function downloadFromYoutube(command) { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + reject({ error, stdout, stderr }); + } else { + resolve({ stdout, stderr }); + } }); -} + }); +}; module.exports = (bot) => { - bot.command('yt', async (ctx) => { - const Strings = getStrings(ctx.from.language_code); - const args = ctx.message.text.split(' ').slice(1).join(' '); - const ytCommand = 'yt-dlp ' + args + ' -o video.mp4'; - ctx.reply(Strings.downloading); - await DownloadFromYoutube(ytCommand); - try { - ctx.reply(Strings.uploading); - await ctx.replyWithVideo({ source: 'video.mp4' }); - } catch (error) { - ctx.reply('Error!') - } - fs.unlinkSync('video.mp4'); - }) -} \ No newline at end of file + bot.command('yt', spamwatchMiddleware, async (ctx) => { + const strings = getStrings(ctx.from.language_code); + const ytDlpPath = getYtDlpPath(); + const userId = ctx.from.id; + const videoUrl = ctx.message.text.split(' ').slice(1).join(' '); + + const mp4File = `tmp/${userId}.mp4`; + const cmdArgs = "--max-filesize 2G --no-playlist --merge-output-format mp4 -o"; + const videoFormat = "-f bestvideo+bestaudio"; + const dlpCommand = `${ytDlpPath} ${videoUrl} ${videoFormat} ${cmdArgs} ${mp4File}`; + + const downloadingMessage = await ctx.reply(strings.ytDownloading, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }); + + try { + await downloadFromYoutube(dlpCommand); + + await ctx.telegram.editMessageText( + ctx.chat.id, + downloadingMessage.message_id, + null, + strings.ytUploading, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }); + + if (fs.existsSync(mp4File)) { + const userId = parseInt(ctx.match[2]); + const userName = ctx.from.first_name; + const message = strings.ytUploadDesc + .replace("{userId}", userId) + .replace("{userName}", userName); + + await ctx.replyWithVideo({ + source: mp4File, + caption: `${message}`, + parse_mode: 'Markdown' + }); + + fs.unlinkSync(mp4File); + } + } catch (error) { + fs.unlinkSync(mp4File); + const message = strings.ytDownloadErr + .replace("{err}", error) + .replace("{userName}", ctx.from.first_name); + + ctx.reply(message, { + parse_mode: 'Markdown', + reply_to_message_id: ctx.message.message_id, + }); + } + }); +}; \ No newline at end of file diff --git a/locales/english.json b/locales/english.json index 2967e95..e2c0042 100644 --- a/locales/english.json +++ b/locales/english.json @@ -58,6 +58,8 @@ "goBack": "Back", "maInvalidModule": "Please provide a valid module ID from The Mod Archive.\nExample: `/modarchive 81574`", "maDownloadError": "Error downloading the file. Check the module ID and try again.", - "downloading": "Downloading...", - "uploading": "Uploading. Please, hold on..." + "ytDownloading": "*Downloading video...*", + "ytUploading": "*Uploading video...*", + "ytUploadDesc": "*[{userName}](tg://user?id={userId}), there is your downloaded video.*", + "ytDownloadErr": "*Error during YT video download:*\n\n`{err}`" } \ No newline at end of file diff --git a/locales/portuguese.json b/locales/portuguese.json index c106deb..a1c9e31 100644 --- a/locales/portuguese.json +++ b/locales/portuguese.json @@ -58,6 +58,8 @@ "goBack": "Voltar", "maInvalidModule": "Por favor, forneça um ID de módulo válido do The Mod Archive.\nExemplo: `/modarchive 81574`", "maDownloadError": "Erro ao baixar o arquivo. Verifique o ID do módulo e tente novamente.", - "downloading": "Fazendo download...", - "uploading": "Fazendo upload. Por favor, aguarde um momento..." + "ytDownloading": "*Baixando vídeo...*", + "ytUploading": "*Enviando video...*", + "ytUploadDesc": "*[{userName}](tg://user?id={userId}), aqui está o seu vídeo baixado.*", + "ytDownloadErr": "*Erro durante o download do vídeo do YT:*\n\n`{err}`" } \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index 35b7bb0..1290d5c 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,3 +1,3 @@ { - "ignore": ["props"] + "ignore": ["props", "tmp"] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 52594b0..d55c88c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.7.7", "child_process": "^1.0.2", "commander": "^12.1.0", + "fluent-ffmpeg": "^2.1.3", "node-html-parser": "^6.1.13", "nodemon": "^3.1.4", "telegraf": "^4.16.3", @@ -421,6 +422,36 @@ "node": ">=8" } }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fluent-ffmpeg/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/fluent-ffmpeg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -577,6 +608,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", diff --git a/package.json b/package.json index c689508..897c0fb 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "axios": "^1.7.7", "child_process": "^1.0.2", "commander": "^12.1.0", + "fluent-ffmpeg": "^2.1.3", "node-html-parser": "^6.1.13", "nodemon": "^3.1.4", "telegraf": "^4.16.3", diff --git a/plugins/ytdlp-wrapper.js b/plugins/ytdlp-wrapper.js new file mode 100644 index 0000000..c233bac --- /dev/null +++ b/plugins/ytdlp-wrapper.js @@ -0,0 +1,55 @@ +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const downloadDir = path.resolve(__dirname, 'yt-dlp'); + +const urls = { + linux: 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp', + win32: 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe', + darwin: 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos', +}; + +function getDownloadUrl() { + const platform = os.platform(); + return urls[platform] || urls.linux; +}; + +async function downloadYtDlp() { + const url = getDownloadUrl(); + const fileName = url.split('/').pop(); + const filePath = path.join(downloadDir, fileName); + + if (!fs.existsSync(downloadDir)) { + fs.mkdirSync(downloadDir, { recursive: true }); + }; + + if (!fs.existsSync(filePath)) { + try { + const response = await axios({ + url, + method: 'GET', + responseType: 'stream', + }); + + const writer = fs.createWriteStream(filePath); + + response.data.pipe(writer); + + writer.on('finish', () => { + if (os.platform() !== 'win32') { + fs.chmodSync(filePath, '755'); + } + }); + + writer.on('error', (err) => { + console.error('WARN: yt-dlp download failed:', err); + }); + } catch (err) { + console.error('WARN: yt-dlp download failed:', err.message); + }; + }; +}; + +downloadYtDlp(); \ No newline at end of file