upload
Browse files- index.js +189 -85
- scrape/ytdown.js +52 -95
index.js
CHANGED
|
@@ -3,7 +3,8 @@ import fs from 'fs';
|
|
| 3 |
import path from 'path';
|
| 4 |
import cors from 'cors';
|
| 5 |
import { fileURLToPath } from 'url';
|
| 6 |
-
import
|
|
|
|
| 7 |
import extractAndDownloadTikTok from './scrape/tiktokdl.js';
|
| 8 |
|
| 9 |
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -12,16 +13,11 @@ const __dirname = path.dirname(__filename);
|
|
| 12 |
const app = express();
|
| 13 |
const PORT = process.env.PORT || 7860;
|
| 14 |
const domain = process.env.DOMAIN || `https://fourstore-ytdl-api.hf.space`;
|
| 15 |
-
const LIB_FOLDER = path.join(__dirname, 'downloads');
|
| 16 |
|
| 17 |
let visitCount = 0;
|
| 18 |
let successDownloads = 0;
|
| 19 |
let failedDownloads = 0;
|
| 20 |
|
| 21 |
-
if (!fs.existsSync(LIB_FOLDER)) {
|
| 22 |
-
fs.mkdirSync(LIB_FOLDER, { recursive: true });
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
app.use(cors({
|
| 26 |
origin: '*',
|
| 27 |
methods: ['GET', 'POST', 'OPTIONS'],
|
|
@@ -30,76 +26,182 @@ app.use(cors({
|
|
| 30 |
|
| 31 |
app.use(express.json());
|
| 32 |
app.use('/image', express.static(path.join(__dirname, 'image')));
|
| 33 |
-
app.use('/download', express.static(LIB_FOLDER));
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
try {
|
| 37 |
-
const
|
| 38 |
-
const result = await ytdown(url, qualityParam);
|
| 39 |
|
| 40 |
-
if (
|
| 41 |
-
throw new Error(
|
| 42 |
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
let
|
| 46 |
-
let fileName = '';
|
| 47 |
-
let fileExt = '';
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
const buffer = await response.arrayBuffer();
|
| 65 |
-
fs.writeFileSync(filePath, Buffer.from(buffer));
|
| 66 |
|
| 67 |
successDownloads++;
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
| 80 |
} catch (error) {
|
| 81 |
failedDownloads++;
|
| 82 |
-
|
| 83 |
-
success: false,
|
| 84 |
-
error: `Failed to download ${type} from YouTube: ${error.message}`
|
| 85 |
-
};
|
| 86 |
}
|
| 87 |
-
}
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
|
|
|
| 91 |
if (!url) return res.status(400).json({ error: "YouTube URL required" });
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
});
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
|
|
|
| 98 |
if (!url) return res.status(400).json({ error: "YouTube URL required" });
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
});
|
| 102 |
|
|
|
|
| 103 |
app.get('/api/download/tiktok', async (req, res) => {
|
| 104 |
try {
|
| 105 |
const { url } = req.query;
|
|
@@ -113,6 +215,7 @@ app.get('/api/download/tiktok', async (req, res) => {
|
|
| 113 |
}
|
| 114 |
});
|
| 115 |
|
|
|
|
| 116 |
app.get('/', (req, res) => {
|
| 117 |
visitCount++;
|
| 118 |
res.send(`<!DOCTYPE html>
|
|
@@ -353,12 +456,12 @@ app.get('/', (req, res) => {
|
|
| 353 |
else endpoint = 'tiktok';
|
| 354 |
|
| 355 |
const response = await fetch(domain + '/api/download/' + endpoint + '?' + params);
|
| 356 |
-
const hasil = await response.json();
|
| 357 |
|
| 358 |
document.getElementById('loading').style.display = 'none';
|
| 359 |
|
| 360 |
-
if (
|
| 361 |
-
|
|
|
|
| 362 |
let html = '<div class="result-card">';
|
| 363 |
if (hasil.type === 'video') {
|
| 364 |
html += '<h3>🎬 Video TikTok</h3>';
|
|
@@ -377,17 +480,28 @@ app.get('/', (req, res) => {
|
|
| 377 |
}
|
| 378 |
html += '</div>';
|
| 379 |
document.getElementById('hasil').innerHTML = html;
|
|
|
|
|
|
|
| 380 |
} else {
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}
|
|
|
|
|
|
|
| 384 |
let berhasilVal = parseInt(document.getElementById('berhasilDiunduh').textContent) + 1;
|
| 385 |
document.getElementById('berhasilDiunduh').textContent = berhasilVal;
|
| 386 |
-
} else {
|
| 387 |
-
document.getElementById('hasil').innerHTML = '<div class="result-card" style="color:#ffcccc;">❌ ' + (hasil.error || 'Gagal download') + '</div>';
|
| 388 |
-
let gagalVal = parseInt(document.getElementById('gagalDiunduh').textContent) + 1;
|
| 389 |
-
document.getElementById('gagalDiunduh').textContent = gagalVal;
|
| 390 |
}
|
|
|
|
| 391 |
let kunjungan = parseInt(document.getElementById('jumlahKunjungan').textContent) + 1;
|
| 392 |
document.getElementById('jumlahKunjungan').textContent = kunjungan;
|
| 393 |
} catch (error) {
|
|
@@ -514,42 +628,32 @@ app.get('/docs', (req, res) => {
|
|
| 514 |
|
| 515 |
<div class="endpoint">
|
| 516 |
<h2><span class="method">GET</span> /api/download/audio</h2>
|
| 517 |
-
<p>Download audio dari YouTube (MP3).</p>
|
| 518 |
<h3>Parameter:</h3>
|
| 519 |
-
<ul><li><code>url</code> (required) - URL YouTube</li></ul>
|
| 520 |
<h3>Contoh:</h3>
|
| 521 |
<pre>GET ${domain}/api/download/audio?url=https://youtube.com/watch?v=VIDEO_ID</pre>
|
| 522 |
</div>
|
| 523 |
|
| 524 |
<div class="endpoint">
|
| 525 |
<h2><span class="method">GET</span> /api/download/video</h2>
|
| 526 |
-
<p>Download video dari YouTube.</p>
|
| 527 |
<h3>Parameter:</h3>
|
| 528 |
-
<ul><li><code>url</code> (required) - URL YouTube</li><li><code>quality</code> (optional) - 144/240/360/480/720/1080 (default:
|
| 529 |
-
<pre>GET ${domain}/api/download/video?url=https://youtube.com/watch?v=VIDEO_ID&quality=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
</div>
|
| 531 |
|
| 532 |
<div class="endpoint">
|
| 533 |
<h2><span class="method">GET</span> /api/download/tiktok</h2>
|
| 534 |
<p>Download video/slide TikTok.</p>
|
| 535 |
-
<h3>Parameter:</h3>
|
| 536 |
-
<ul><li><code>url</code> (required) - URL TikTok</li></ul>
|
| 537 |
<pre>GET ${domain}/api/download/tiktok?url=https://tiktok.com/@user/video/123456789</pre>
|
| 538 |
</div>
|
| 539 |
-
|
| 540 |
-
<h2>📦 Response Format</h2>
|
| 541 |
-
<h3>YouTube Audio/Video:</h3>
|
| 542 |
-
<pre>{
|
| 543 |
-
"success": true,
|
| 544 |
-
"type": "AUDIO/VIDEO",
|
| 545 |
-
"title": "Judul Video",
|
| 546 |
-
"channel": "Nama Channel",
|
| 547 |
-
"duration": "3:45",
|
| 548 |
-
"thumbnail": "https://...",
|
| 549 |
-
"quality": "720",
|
| 550 |
-
"size": "4.56 MB",
|
| 551 |
-
"download_url": "https://..."
|
| 552 |
-
}</pre>
|
| 553 |
</div>
|
| 554 |
<footer>Made with ❤️ by Fourstore | Owner: RezaHaris | <a href="https://wa.me/6283163784116">Contact</a></footer>
|
| 555 |
<script>
|
|
|
|
| 3 |
import path from 'path';
|
| 4 |
import cors from 'cors';
|
| 5 |
import { fileURLToPath } from 'url';
|
| 6 |
+
import axios from 'axios';
|
| 7 |
+
import YTDown from './scrape/ytdown.js';
|
| 8 |
import extractAndDownloadTikTok from './scrape/tiktokdl.js';
|
| 9 |
|
| 10 |
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
| 13 |
const app = express();
|
| 14 |
const PORT = process.env.PORT || 7860;
|
| 15 |
const domain = process.env.DOMAIN || `https://fourstore-ytdl-api.hf.space`;
|
|
|
|
| 16 |
|
| 17 |
let visitCount = 0;
|
| 18 |
let successDownloads = 0;
|
| 19 |
let failedDownloads = 0;
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
app.use(cors({
|
| 22 |
origin: '*',
|
| 23 |
methods: ['GET', 'POST', 'OPTIONS'],
|
|
|
|
| 26 |
|
| 27 |
app.use(express.json());
|
| 28 |
app.use('/image', express.static(path.join(__dirname, 'image')));
|
|
|
|
| 29 |
|
| 30 |
+
const ytdown = new YTDown();
|
| 31 |
+
|
| 32 |
+
// Endpoint untuk stream audio langsung
|
| 33 |
+
app.get('/api/download/audio', async (req, res) => {
|
| 34 |
+
const { url, quality = '128K' } = req.query;
|
| 35 |
+
if (!url) return res.status(400).json({ error: "YouTube URL required" });
|
| 36 |
+
|
| 37 |
try {
|
| 38 |
+
const info = await ytdown.getVideoInfo(url);
|
|
|
|
| 39 |
|
| 40 |
+
if (info?.api?.status !== 'ok') {
|
| 41 |
+
throw new Error(info?.api?.message || 'Gagal mendapatkan info video');
|
| 42 |
}
|
| 43 |
|
| 44 |
+
let audioUrl = null;
|
| 45 |
+
let audioInfo = null;
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
for (const item of info.api.mediaItems) {
|
| 48 |
+
if (item.type === 'Audio') {
|
| 49 |
+
if (item.mediaQuality === quality) {
|
| 50 |
+
audioUrl = item.mediaPreviewUrl;
|
| 51 |
+
audioInfo = item;
|
| 52 |
+
break;
|
| 53 |
+
}
|
| 54 |
+
if (!audioUrl) {
|
| 55 |
+
audioUrl = item.mediaPreviewUrl;
|
| 56 |
+
audioInfo = item;
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
}
|
| 60 |
|
| 61 |
+
if (!audioUrl) {
|
| 62 |
+
throw new Error('Audio tidak ditemukan');
|
| 63 |
+
}
|
|
|
|
|
|
|
| 64 |
|
| 65 |
successDownloads++;
|
| 66 |
|
| 67 |
+
const response = await axios({
|
| 68 |
+
method: 'GET',
|
| 69 |
+
url: audioUrl,
|
| 70 |
+
responseType: 'stream',
|
| 71 |
+
headers: {
|
| 72 |
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
|
| 73 |
+
}
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
const filename = `${info.api.title}.mp3`.replace(/[^a-z0-9.-]/gi, '_');
|
| 77 |
+
res.setHeader('Content-Type', 'audio/mpeg');
|
| 78 |
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
| 79 |
+
response.data.pipe(res);
|
| 80 |
+
|
| 81 |
} catch (error) {
|
| 82 |
failedDownloads++;
|
| 83 |
+
res.status(500).json({ success: false, error: error.message });
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
+
});
|
| 86 |
|
| 87 |
+
// Endpoint untuk stream video langsung
|
| 88 |
+
app.get('/api/download/video', async (req, res) => {
|
| 89 |
+
const { url, quality = 'HD' } = req.query;
|
| 90 |
if (!url) return res.status(400).json({ error: "YouTube URL required" });
|
| 91 |
+
|
| 92 |
+
try {
|
| 93 |
+
const info = await ytdown.getVideoInfo(url);
|
| 94 |
+
|
| 95 |
+
if (info?.api?.status !== 'ok') {
|
| 96 |
+
throw new Error(info?.api?.message || 'Gagal mendapatkan info video');
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
let videoUrl = null;
|
| 100 |
+
let videoInfo = null;
|
| 101 |
+
|
| 102 |
+
const qualityMap = {
|
| 103 |
+
'1080': 'FHD',
|
| 104 |
+
'fhd': 'FHD',
|
| 105 |
+
'720': 'HD',
|
| 106 |
+
'hd': 'HD',
|
| 107 |
+
'480': 'SD',
|
| 108 |
+
'360': 'SD',
|
| 109 |
+
'240': 'SD',
|
| 110 |
+
'144': 'SD'
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
const targetQuality = qualityMap[String(quality).toLowerCase()] || quality;
|
| 114 |
+
|
| 115 |
+
for (const item of info.api.mediaItems) {
|
| 116 |
+
if (item.type === 'Video') {
|
| 117 |
+
if (item.mediaQuality === targetQuality) {
|
| 118 |
+
videoUrl = item.mediaPreviewUrl;
|
| 119 |
+
videoInfo = item;
|
| 120 |
+
break;
|
| 121 |
+
}
|
| 122 |
+
if (!videoUrl) {
|
| 123 |
+
videoUrl = item.mediaPreviewUrl;
|
| 124 |
+
videoInfo = item;
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
if (!videoUrl) {
|
| 130 |
+
throw new Error('Video tidak ditemukan');
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
successDownloads++;
|
| 134 |
+
|
| 135 |
+
const response = await axios({
|
| 136 |
+
method: 'GET',
|
| 137 |
+
url: videoUrl,
|
| 138 |
+
responseType: 'stream',
|
| 139 |
+
headers: {
|
| 140 |
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
|
| 141 |
+
}
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
const filename = `${info.api.title}.mp4`.replace(/[^a-z0-9.-]/gi, '_');
|
| 145 |
+
res.setHeader('Content-Type', 'video/mp4');
|
| 146 |
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
| 147 |
+
response.data.pipe(res);
|
| 148 |
+
|
| 149 |
+
} catch (error) {
|
| 150 |
+
failedDownloads++;
|
| 151 |
+
res.status(500).json({ success: false, error: error.message });
|
| 152 |
+
}
|
| 153 |
});
|
| 154 |
|
| 155 |
+
// Endpoint untuk get info video
|
| 156 |
+
app.get('/api/info', async (req, res) => {
|
| 157 |
+
const { url } = req.query;
|
| 158 |
if (!url) return res.status(400).json({ error: "YouTube URL required" });
|
| 159 |
+
|
| 160 |
+
try {
|
| 161 |
+
const info = await ytdown.getVideoInfo(url);
|
| 162 |
+
|
| 163 |
+
if (info?.api?.status !== 'ok') {
|
| 164 |
+
throw new Error(info?.api?.message || 'Gagal mendapatkan info video');
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
const videos = [];
|
| 168 |
+
const audios = [];
|
| 169 |
+
|
| 170 |
+
for (const item of info.api.mediaItems) {
|
| 171 |
+
if (item.type === 'Video') {
|
| 172 |
+
videos.push({
|
| 173 |
+
quality: item.mediaQuality,
|
| 174 |
+
size: item.mediaFileSize,
|
| 175 |
+
resolution: item.mediaRes,
|
| 176 |
+
url: item.mediaPreviewUrl
|
| 177 |
+
});
|
| 178 |
+
} else if (item.type === 'Audio') {
|
| 179 |
+
audios.push({
|
| 180 |
+
quality: item.mediaQuality,
|
| 181 |
+
size: item.mediaFileSize,
|
| 182 |
+
extension: item.mediaExtension,
|
| 183 |
+
url: item.mediaPreviewUrl
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
res.json({
|
| 189 |
+
success: true,
|
| 190 |
+
title: info.api.title,
|
| 191 |
+
channel: info.api.userInfo?.name,
|
| 192 |
+
duration: info.api.mediaItems?.[0]?.mediaDuration,
|
| 193 |
+
thumbnail: info.api.imagePreviewUrl,
|
| 194 |
+
description: info.api.description,
|
| 195 |
+
videos: videos,
|
| 196 |
+
audios: audios
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
} catch (error) {
|
| 200 |
+
res.status(500).json({ success: false, error: error.message });
|
| 201 |
+
}
|
| 202 |
});
|
| 203 |
|
| 204 |
+
// TikTok endpoint
|
| 205 |
app.get('/api/download/tiktok', async (req, res) => {
|
| 206 |
try {
|
| 207 |
const { url } = req.query;
|
|
|
|
| 215 |
}
|
| 216 |
});
|
| 217 |
|
| 218 |
+
// Halaman utama
|
| 219 |
app.get('/', (req, res) => {
|
| 220 |
visitCount++;
|
| 221 |
res.send(`<!DOCTYPE html>
|
|
|
|
| 456 |
else endpoint = 'tiktok';
|
| 457 |
|
| 458 |
const response = await fetch(domain + '/api/download/' + endpoint + '?' + params);
|
|
|
|
| 459 |
|
| 460 |
document.getElementById('loading').style.display = 'none';
|
| 461 |
|
| 462 |
+
if (jenis === 'tiktok') {
|
| 463 |
+
const hasil = await response.json();
|
| 464 |
+
if (hasil.success) {
|
| 465 |
let html = '<div class="result-card">';
|
| 466 |
if (hasil.type === 'video') {
|
| 467 |
html += '<h3>🎬 Video TikTok</h3>';
|
|
|
|
| 480 |
}
|
| 481 |
html += '</div>';
|
| 482 |
document.getElementById('hasil').innerHTML = html;
|
| 483 |
+
let berhasilVal = parseInt(document.getElementById('berhasilDiunduh').textContent) + 1;
|
| 484 |
+
document.getElementById('berhasilDiunduh').textContent = berhasilVal;
|
| 485 |
} else {
|
| 486 |
+
document.getElementById('hasil').innerHTML = '<div class="result-card" style="color:#ffcccc;">❌ ' + (hasil.error || 'Gagal download') + '</div>';
|
| 487 |
+
let gagalVal = parseInt(document.getElementById('gagalDiunduh').textContent) + 1;
|
| 488 |
+
document.getElementById('gagalDiunduh').textContent = gagalVal;
|
| 489 |
+
}
|
| 490 |
+
} else {
|
| 491 |
+
const blob = await response.blob();
|
| 492 |
+
const downloadUrl = URL.createObjectURL(blob);
|
| 493 |
+
const contentDisposition = response.headers.get('Content-Disposition');
|
| 494 |
+
let filename = jenis === 'audio' ? 'audio.mp3' : 'video.mp4';
|
| 495 |
+
if (contentDisposition) {
|
| 496 |
+
const match = contentDisposition.match(/filename="(.+)"/);
|
| 497 |
+
if (match) filename = match[1];
|
| 498 |
}
|
| 499 |
+
|
| 500 |
+
document.getElementById('hasil').innerHTML = '<div class="result-card"><a href="' + downloadUrl + '" class="download-btn" download="' + filename + '">⬇️ Download ' + (jenis === 'audio' ? 'Audio' : 'Video') + '</a></div>';
|
| 501 |
let berhasilVal = parseInt(document.getElementById('berhasilDiunduh').textContent) + 1;
|
| 502 |
document.getElementById('berhasilDiunduh').textContent = berhasilVal;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
}
|
| 504 |
+
|
| 505 |
let kunjungan = parseInt(document.getElementById('jumlahKunjungan').textContent) + 1;
|
| 506 |
document.getElementById('jumlahKunjungan').textContent = kunjungan;
|
| 507 |
} catch (error) {
|
|
|
|
| 628 |
|
| 629 |
<div class="endpoint">
|
| 630 |
<h2><span class="method">GET</span> /api/download/audio</h2>
|
| 631 |
+
<p>Download audio dari YouTube (MP3) - LANGSUNG STREAM.</p>
|
| 632 |
<h3>Parameter:</h3>
|
| 633 |
+
<ul><li><code>url</code> (required) - URL YouTube</li><li><code>quality</code> (optional) - 48K / 128K (default: 128K)</li></ul>
|
| 634 |
<h3>Contoh:</h3>
|
| 635 |
<pre>GET ${domain}/api/download/audio?url=https://youtube.com/watch?v=VIDEO_ID</pre>
|
| 636 |
</div>
|
| 637 |
|
| 638 |
<div class="endpoint">
|
| 639 |
<h2><span class="method">GET</span> /api/download/video</h2>
|
| 640 |
+
<p>Download video dari YouTube - LANGSUNG STREAM.</p>
|
| 641 |
<h3>Parameter:</h3>
|
| 642 |
+
<ul><li><code>url</code> (required) - URL YouTube</li><li><code>quality</code> (optional) - 144/240/360/480/720/1080 atau SD/HD/FHD (default: HD)</li></ul>
|
| 643 |
+
<pre>GET ${domain}/api/download/video?url=https://youtube.com/watch?v=VIDEO_ID&quality=720</pre>
|
| 644 |
+
</div>
|
| 645 |
+
|
| 646 |
+
<div class="endpoint">
|
| 647 |
+
<h2><span class="method">GET</span> /api/info</h2>
|
| 648 |
+
<p>Get video info tanpa download.</p>
|
| 649 |
+
<pre>GET ${domain}/api/info?url=https://youtube.com/watch?v=VIDEO_ID</pre>
|
| 650 |
</div>
|
| 651 |
|
| 652 |
<div class="endpoint">
|
| 653 |
<h2><span class="method">GET</span> /api/download/tiktok</h2>
|
| 654 |
<p>Download video/slide TikTok.</p>
|
|
|
|
|
|
|
| 655 |
<pre>GET ${domain}/api/download/tiktok?url=https://tiktok.com/@user/video/123456789</pre>
|
| 656 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
</div>
|
| 658 |
<footer>Made with ❤️ by Fourstore | Owner: RezaHaris | <a href="https://wa.me/6283163784116">Contact</a></footer>
|
| 659 |
<script>
|
scrape/ytdown.js
CHANGED
|
@@ -1,105 +1,62 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
| 3 |
-
import
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
const { data } = await axios.post(
|
| 27 |
-
"https://app.ytdown.to/proxy.php",
|
| 28 |
-
qs.stringify({ url }),
|
| 29 |
-
{ headers, timeout: 20000 }
|
| 30 |
-
)
|
| 31 |
-
return data?.api?.status === "completed" ? data.api : null
|
| 32 |
-
} catch {
|
| 33 |
-
return null
|
| 34 |
}
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
export default async function ytdown(url, quality = "720") {
|
| 38 |
-
try {
|
| 39 |
-
const { data } = await axios.post(
|
| 40 |
-
"https://app.ytdown.to/proxy.php",
|
| 41 |
-
qs.stringify({ url }),
|
| 42 |
-
{ headers, timeout: 20000 }
|
| 43 |
-
)
|
| 44 |
-
|
| 45 |
-
if (data?.api?.status !== "ok") return { status: false }
|
| 46 |
-
|
| 47 |
-
const targetQ = normalizeQ(quality)
|
| 48 |
-
const isAudioOnly = /mp3|audio/i.test(quality)
|
| 49 |
-
|
| 50 |
-
let selectedVideo = null
|
| 51 |
-
let fallbackVideo = null
|
| 52 |
-
let bestAudio = null
|
| 53 |
-
let fallbackAudio = null
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
const ext = res.fileName?.split(".").pop()?.toLowerCase()
|
| 60 |
-
|
| 61 |
-
const obj = {
|
| 62 |
-
quality: item.mediaQuality,
|
| 63 |
-
url: res.fileUrl,
|
| 64 |
-
size: res.fileSize,
|
| 65 |
-
ext,
|
| 66 |
-
mime: item.type === "Video" ? "video/" + ext : "audio/" + ext
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
if (item.type === "Video") {
|
| 70 |
-
const qNum = normalizeQ(item.mediaQuality)
|
| 71 |
-
|
| 72 |
-
if (qNum == targetQ && !selectedVideo) {
|
| 73 |
-
selectedVideo = obj
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
if (!fallbackVideo || Number(qNum) > Number(normalizeQ(fallbackVideo.quality))) {
|
| 77 |
-
fallbackVideo = obj
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
}
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
-
}
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
duration: data.api.mediaItems?.[0]?.mediaDuration,
|
| 98 |
-
video: isAudioOnly ? null : (selectedVideo || fallbackVideo),
|
| 99 |
-
audio: bestAudio || fallbackAudio
|
| 100 |
}
|
| 101 |
|
| 102 |
-
|
| 103 |
-
return { status: false, error: String(e) }
|
| 104 |
}
|
| 105 |
}
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from 'axios';
|
| 2 |
+
import FormData from 'form-data';
|
| 3 |
+
import { randomUUID } from 'crypto';
|
| 4 |
+
|
| 5 |
+
class YTDown {
|
| 6 |
+
constructor() {
|
| 7 |
+
this.baseUrl = 'https://app.ytdown.to';
|
| 8 |
+
this.headers = {
|
| 9 |
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
|
| 10 |
+
'Accept': '*/*',
|
| 11 |
+
'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7',
|
| 12 |
+
'Origin': this.baseUrl,
|
| 13 |
+
'Referer': this.baseUrl + '/id15/',
|
| 14 |
+
'sec-ch-ua': '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
|
| 15 |
+
'sec-ch-ua-mobile': '?0',
|
| 16 |
+
'sec-ch-ua-platform': '"Linux"',
|
| 17 |
+
'sec-fetch-dest': 'empty',
|
| 18 |
+
'sec-fetch-mode': 'cors',
|
| 19 |
+
'sec-fetch-site': 'same-origin',
|
| 20 |
+
'x-requested-with': 'XMLHttpRequest'
|
| 21 |
+
};
|
| 22 |
+
this.cookies = {
|
| 23 |
+
'PHPSESSID': randomUUID().replace(/-/g, ''),
|
| 24 |
+
'_ga': 'GA1.1.' + Math.floor(Math.random() * 1000000000) + '.' + Math.floor(Date.now() / 1000)
|
| 25 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
generateSession() {
|
| 29 |
+
this.cookies.PHPSESSID = randomUUID().replace(/-/g, '');
|
| 30 |
+
this.cookies._ga = 'GA1.1.' + Math.floor(Math.random() * 1000000000) + '.' + Math.floor(Date.now() / 1000);
|
| 31 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
getCookieString() {
|
| 34 |
+
return Object.entries(this.cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
| 35 |
+
}
|
|
|
|
| 36 |
|
| 37 |
+
async getVideoInfo(url) {
|
| 38 |
+
this.generateSession();
|
| 39 |
+
|
| 40 |
+
const form = new FormData();
|
| 41 |
+
form.append('url', url);
|
| 42 |
+
|
| 43 |
+
const response = await axios.post(`${this.baseUrl}/proxy.php`, form, {
|
| 44 |
+
headers: {
|
| 45 |
+
...this.headers,
|
| 46 |
+
...form.getHeaders(),
|
| 47 |
+
'Cookie': this.getCookieString()
|
| 48 |
}
|
| 49 |
+
});
|
| 50 |
|
| 51 |
+
if (response.headers['set-cookie']) {
|
| 52 |
+
response.headers['set-cookie'].forEach(cookie => {
|
| 53 |
+
const [key, value] = cookie.split(';')[0].split('=');
|
| 54 |
+
this.cookies[key] = value;
|
| 55 |
+
});
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
+
return response.data;
|
|
|
|
| 59 |
}
|
| 60 |
}
|
| 61 |
+
|
| 62 |
+
export default YTDown;
|