const express = require('express'); const cors = require('cors'); const multer = require('multer'); const { exec, spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const { PDFDocument } = require('pdf-lib'); const archiver = require('archiver'); const app = express(); const port = 3000; // CORS AYARLARI app.use(cors()); app.use(express.json()); // Sayaç (İstatistik) Mantığı const statsFile = path.join(__dirname, 'stats.json'); let stats = { conversions: 8524 }; if (fs.existsSync(statsFile)) { try { stats = JSON.parse(fs.readFileSync(statsFile, 'utf8')); } catch (e) {} } const incrementStats = () => { stats.conversions += 1; fs.writeFileSync(statsFile, JSON.stringify(stats)); }; app.get('/api/stats', (req, res) => { res.json(stats); }); // Sağlık Kontrolü (Dependency check) app.get('/api/health', (req, res) => { const checks = { libreoffice: false, ytdlp: false, ffmpeg: false }; exec('libreoffice --version', (err) => { if (!err) checks.libreoffice = true; exec('yt-dlp --version', (err) => { if (!err) checks.ytdlp = true; exec('ffmpeg -version', (err) => { if (!err) checks.ffmpeg = true; res.json(checks); }); }); }); }); // Geçici dosyalar için yükleme dizini const UPLOAD_DIR = '/tmp/convertcom_uploads/'; const OUTPUT_DIR = '/tmp/convertcom_outputs/'; [UPLOAD_DIR, OUTPUT_DIR].forEach(dir => { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); }); const upload = multer({ dest: UPLOAD_DIR, limits: { fileSize: 50 * 1024 * 1024 } // 50MB Limit }); // ---------------------------------------------------- // 1. DÖNÜŞTÜRÜCÜ: PDF (LibreOffice + ImageMagick) // ---------------------------------------------------- app.post('/api/convert/pdf', upload.single('File'), (req, res) => { if (!req.file) return res.status(400).json({ error: 'Dosya yüklenmedi.' }); const inputPath = req.file.path; const originalName = req.file.originalname; const ext = path.extname(originalName).toLowerCase(); const safeName = originalName.replace(/[^a-zA-Z0-9.-]/g, '_'); const inputWithExt = inputPath + ext; fs.renameSync(inputPath, inputWithExt); const outputPdfName = path.parse(safeName).name + '_' + Date.now() + '.pdf'; const outputPath = path.join(OUTPUT_DIR, outputPdfName); let command; const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp', '.heic', '.heif']; // Telefondan direkt çekilen fotoğraflarda uzantı olmayabilir ama mimetype 'image/jpeg' olabilir const isImage = imageExtensions.includes(ext) || (req.file.mimetype && req.file.mimetype.startsWith('image/')); if (isImage) { // Telefon fotoğrafları (iOS/Android) bazen yan durur, -auto-orient bunu otomatik düzeltir. command = `convert "${inputWithExt}" -auto-orient -quality 85 "${outputPath}"`; } else { // LibreOffice --headless command = `libreoffice --headless --convert-to pdf "${inputWithExt}" --outdir "${OUTPUT_DIR}"`; } console.log(`[PDF] Başlatıldı: ${originalName} (Resim Formatı: ${isImage ? 'Evet' : 'Hayır'})`); exec(command, (error, stdout, stderr) => { let finalPdfPath = outputPath; if (!isImage) { // LibreOffice çıktıyı kendi belirlediği isimle atar const expectedName = path.parse(inputWithExt).name + '.pdf'; finalPdfPath = path.join(OUTPUT_DIR, expectedName); } // --- HATA VE YEDEK (FALLBACK) SİSTEMİ --- if (error || !fs.existsSync(finalPdfPath)) { console.error('[PDF] Ana Dönüştürücü Hatası:', error ? error.message : 'Dosya oluşmadı', stderr); // Eğer resim ise ve ImageMagick patladıysa (Örn: VPS ImageMagick güvenlik policy hatası) // LibreOffice'i yedek olarak kullanalım (HEIC hariç, çünkü LO HEIC açamaz) if (isImage && ext !== '.heic' && ext !== '.heif') { console.log('[PDF] ImageMagick başarısız oldu, LibreOffice ile yedek deneme yapılıyor...'); const fallbackCmd = `libreoffice --headless --convert-to pdf "${inputWithExt}" --outdir "${OUTPUT_DIR}"`; exec(fallbackCmd, (err2, out2, errOut2) => { const fallbackPath = path.join(OUTPUT_DIR, path.parse(inputWithExt).name + '.pdf'); if (err2 || !fs.existsSync(fallbackPath)) { console.error('[PDF] LibreOffice Yedek Hatası:', err2 ? err2.message : 'Dosya yok'); return res.status(500).json({ error: 'Resim dönüştürülemedi. Dosya boyutu veya formatı sunucu limitlerine takılmış olabilir.' }); } sendPdfResponse(fallbackPath, originalName, inputWithExt); }); return; // Buradan çık, işlemi yedek (fallback) tamamlayacak } return res.status(500).json({ error: 'Dönüştürme başarısız oldu. Dosya desteklenmiyor veya çok büyük olabilir.' }); } // Ana sistem başarılı olduysa gönder sendPdfResponse(finalPdfPath, originalName, inputWithExt); }); // Kod tekrarını önlemek için indirme fonksiyonu function sendPdfResponse(pdfPath, origName, inPath) { res.download(pdfPath, path.parse(origName).name + '.pdf', (err) => { if (!err) incrementStats(); // Temizlik (Çöp dosyalar silinir) [inPath, pdfPath].forEach(p => { try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch(e) {} }); }); } }); // ---------------------------------------------------- // 2. YOUTUBE MP3 DÖNÜŞTÜRÜCÜ (yt-dlp) - GELİŞMİŞ // ---------------------------------------------------- app.post('/api/youtube', (req, res) => { const { url } = req.body; if (!url || (!url.includes('youtube.com') && !url.includes('youtu.be'))) { return res.status(400).json({ error: 'Geçersiz YouTube URL.' }); } const timestamp = Date.now(); const outputTemplate = path.join(OUTPUT_DIR, `${timestamp}_%(title)s.%(ext)s`); // YouTube'un bot engeline karşı farklı player_client'ları sırayla dene // tv_embedded → YouTube TV istemcisi, en az engellenen // mweb → Mobil web, iyi çalışır // web → Normal web const tryDownload = (playerClient, attempt) => { const args = [ '--extractor-args', `youtube:player_client=${playerClient}`, '--no-playlist', '--extract-audio', '--audio-format', 'mp3', '--audio-quality', '5', // 0=en yüksek kalite ama yavaş, 5=dengeli '--no-check-certificate', '--no-warnings', '--prefer-free-formats', '-o', outputTemplate, url ]; console.log(`[YouTube] Deneme ${attempt} - player_client: ${playerClient} - ${url}`); const ytProcess = spawn('yt-dlp', args); let stderrOutput = ''; // 5 dakika timeout (çok uzun videolar için) const timeout = setTimeout(() => { ytProcess.kill('SIGKILL'); console.error('[YouTube] Timeout! 5 dakika aşıldı.'); if (!res.headersSent) { res.status(500).json({ error: 'Video indirme zaman aşımına uğradı. Çok uzun bir video olabilir.' }); } }, 5 * 60 * 1000); ytProcess.stderr.on('data', (data) => { stderrOutput += data.toString(); }); ytProcess.on('close', (code) => { clearTimeout(timeout); if (code !== 0) { console.error(`[YouTube] Hata (${playerClient}):`, stderrOutput.slice(-500)); // Otomatik yeniden dene if (attempt === 1) { console.log('[YouTube] tv_embedded başarısız, mweb ile deneniyor...'); return tryDownload('mweb', 2); } else if (attempt === 2) { console.log('[YouTube] mweb başarısız, web ile deneniyor...'); return tryDownload('web', 3); } // Hata mesajını analiz et let errorMsg = 'Video indirilemedi.'; if (stderrOutput.includes('Video unavailable')) { errorMsg = 'Video mevcut değil veya gizli.'; } else if (stderrOutput.includes('age') || stderrOutput.includes('Sign in')) { errorMsg = 'Bu video yaş kısıtlamalı veya giriş gerektiriyor.'; } else if (stderrOutput.includes('copyright') || stderrOutput.includes('blocked')) { errorMsg = 'Bu video telif hakkı nedeniyle indirilemez.'; } else if (stderrOutput.includes('not a bot')) { errorMsg = 'YouTube bot koruması aktif. Birkaç dakika sonra tekrar deneyin.'; } if (!res.headersSent) { return res.status(500).json({ error: errorMsg }); } return; } // İndirilen dosyayı bul const files = fs.readdirSync(OUTPUT_DIR); const downloadedFile = files.find(f => f.startsWith(`${timestamp}_`) && f.endsWith('.mp3')); if (!downloadedFile) { if (!res.headersSent) { return res.status(500).json({ error: 'Dönüştürülen dosya bulunamadı.' }); } return; } const filePath = path.join(OUTPUT_DIR, downloadedFile); // Dosya adından timestamp'i kaldır, özel karakterleri temizle const cleanName = downloadedFile .replace(`${timestamp}_`, '') .replace(/[^\w\s.-]/g, '_'); // Özel karakterleri temizle console.log(`[YouTube] Başarılı! Gönderiliyor: ${cleanName}`); res.download(filePath, cleanName, (err) => { if (!err) incrementStats(); setTimeout(() => { try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch (e) {} }, 30000); }); }); }; // tv_embedded ile başla (en güvenilir) tryDownload('tv_embedded', 1); }); // ---------------------------------------------------- // 3. PDF BİRLEŞTİR (MERGE) // ---------------------------------------------------- app.post('/api/convert/merge', upload.array('files', 20), async (req, res) => { if (!req.files || req.files.length < 2) { return res.status(400).json({ error: 'Lütfen en az 2 adet PDF dosyası yükleyin.' }); } try { const mergedPdf = await PDFDocument.create(); for (const file of req.files) { // Yüklenen dosyayı oku const pdfBytes = fs.readFileSync(file.path); // Okunan byte'ları PDF olarak yükle const pdf = await PDFDocument.load(pdfBytes); // Tüm sayfaları kopyala const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); // Kopyalanan sayfaları yeni PDF'e ekle copiedPages.forEach((page) => mergedPdf.addPage(page)); } // Birleştirilmiş PDF'i diske kaydet const mergedPdfBytes = await mergedPdf.save(); const outputFilename = `ConvertCom_Merged_${Date.now()}.pdf`; const outputPath = path.join(OUTPUT_DIR, outputFilename); fs.writeFileSync(outputPath, mergedPdfBytes); res.download(outputPath, outputFilename, (err) => { if (!err) incrementStats(); // Geçici yüklenen dosyaları sil req.files.forEach(f => { try { if (fs.existsSync(f.path)) fs.unlinkSync(f.path); } catch(e){} }); // Oluşan PDF'i 30 saniye sonra sil setTimeout(() => { try { if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath); } catch(e){} }, 30000); }); } catch (err) { console.error('[PDF Merge] Hata:', err); res.status(500).json({ error: 'PDF dosyaları birleştirilirken teknik bir sorun oluştu.' }); } }); // ---------------------------------------------------- // 4. PDF BÖL (SPLIT) - (Zipleme Yöntemi) // ---------------------------------------------------- app.post('/api/convert/split', upload.single('File'), async (req, res) => { if (!req.file) { return res.status(400).json({ error: 'Lütfen bölünecek bir PDF dosyası yükleyin.' }); } try { const pdfBytes = fs.readFileSync(req.file.path); const originalPdf = await PDFDocument.load(pdfBytes); const pageCount = originalPdf.getPageCount(); const timestamp = Date.now(); const zipFilename = `ConvertCom_Split_${timestamp}.zip`; const zipPath = path.join(OUTPUT_DIR, zipFilename); const output = fs.createWriteStream(zipPath); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', () => { res.download(zipPath, zipFilename, (err) => { if (!err) incrementStats(); // Temizlik try { if (fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path); } catch (e) {} setTimeout(() => { try { if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath); } catch (e) {} }, 30000); }); }); archive.on('error', (err) => { throw err; }); archive.pipe(output); // Her sayfayı tek tek yeni pdf yap ve zip'e koy for (let i = 0; i < pageCount; i++) { const newPdf = await PDFDocument.create(); const [copiedPage] = await newPdf.copyPages(originalPdf, [i]); newPdf.addPage(copiedPage); const newPdfBytes = await newPdf.save(); archive.append(Buffer.from(newPdfBytes), { name: `Sayfa_${i + 1}.pdf` }); } await archive.finalize(); } catch (err) { console.error('[PDF Split] Hata:', err); try { if (fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path); } catch (e) {} res.status(500).json({ error: 'PDF dosyası bölünürken teknik bir sorun oluştu.' }); } }); // ---------------------------------------------------- // 5. PDF SIKIŞTIR (COMPRESS) - Ghostscript // ---------------------------------------------------- app.post('/api/convert/compress', upload.single('File'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'Lütfen sıkıştırılacak bir PDF dosyası yükleyin.' }); } const inputPath = req.file.path; const inputWithExt = inputPath + '.pdf'; fs.renameSync(inputPath, inputWithExt); const outputFilename = `ConvertCom_Compressed_${Date.now()}.pdf`; const outputPath = path.join(OUTPUT_DIR, outputFilename); // /screen en yüksek sıkıştırma sağlar (düşük boyut) const gsCmd = `gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/screen -dNOPAUSE -dQUIET -dBATCH -sOutputFile="${outputPath}" "${inputWithExt}"`; exec(gsCmd, (error, stdout, stderr) => { if (error || !fs.existsSync(outputPath)) { console.error('[PDF Compress] Ghostscript Hatası:', error ? error.message : 'Çıktı yok', stderr); try { if (fs.existsSync(inputWithExt)) fs.unlinkSync(inputWithExt); } catch (e) {} return res.status(500).json({ error: 'PDF dosyası sıkıştırılamadı. Sistemde Ghostscript yüklü olmayabilir.' }); } res.download(outputPath, outputFilename, (err) => { if (!err) incrementStats(); try { if (fs.existsSync(inputWithExt)) fs.unlinkSync(inputWithExt); } catch (e) {} setTimeout(() => { try { if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath); } catch (e) {} }, 30000); }); }); }); app.listen(port, () => { console.log(`ConvertCom API Aktif: http://localhost:${port}`); });