├── LICENSE ├── README.md ├── package.json ├── public ├── css │ └── style.css ├── index.html └── js │ └── main.js ├── server.js ├── src ├── downloader.js ├── scrape │ ├── capcut.js │ ├── facebook.js │ ├── instagram.js │ ├── snackvideo.js │ ├── soundcloud.js │ ├── spotify.js │ ├── terabox.js │ ├── threads.js │ ├── tiktok.js │ └── xiaohongshu.js └── system │ └── patterns.js └── vercel.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mistra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | Logo 7 | 8 | 9 |

selfhost-dl

10 | 11 |

12 | SelfHost-DL is my project that lets users self-host a simple web downloader with minimal setup. Just run it with Node.js—no third-party services needed. Deploy the files, start the server, and download content easily. Perfect for a lightweight, self-managed solution.. 13 |

14 |
15 | 16 | 17 | 18 |
19 | 20 | ![GitHub Repo stars](https://img.shields.io/github/stars/000mistra000/selfhost-dl?style=for-the-badge) 21 | ![GitHub forks](https://img.shields.io/github/forks/000mistra000/selfhost-dl?style=for-the-badge) 22 | ![GitHub watchers](https://img.shields.io/github/watchers/000mistra000/selfhost-dl?style=for-the-badge) 23 | ![GitHub issues](https://img.shields.io/github/issues/000mistra000/selfhost-dl?style=for-the-badge) 24 | ![GitHub license](https://img.shields.io/github/license/000mistra000/selfhost-dl?style=for-the-badge) 25 | 26 | --------------- 27 | ### **THIS CODE IS FOR EDUCATIONAL PURPOSES ONLY, USE IT WISELY, ANY MISUSE IS NOT THE RESPONSIBILITY OF THE CREATOR.** 28 | 29 | # SCREENSHOT 30 | 31 |
32 | 33 |

34 | 35 | Screenshot-2025-02-16-120806 36 | 37 | 38 | Screenshot-2025-02-16-120812 39 | 40 | 41 | Screenshot-2025-02-16-120836 42 | 43 |

44 |
45 | 46 | --------------- 47 | 48 | ## 🚀 Features 49 | - **Scrape APIs** – Uses various APIs to fetch and download media. 50 | - **Requires Only Node.js** – No database or heavy dependencies needed. 51 | - **Lightweight & Fast** – Optimized for speed and minimal resource usage. 52 | - **Supports multiple platforms:** 53 | - **TikTok**: `TiktokDownloader` 54 | - **CapCut**: `CapcutDownloader` 55 | - **Xiaohongshu**: `XiaohongshuDownloader` 56 | - **Threads**: `ThreadsDownloader` 57 | - **SoundCloud**: `SoundcloudDownloader` 58 | - **Spotify**: `SpotifyDownloader` 59 | - **Instagram**: `InstagramDownloader` 60 | - **Facebook**: `FacebookDownloader` 61 | - **Terabox**: `TeraboxDownloader` 62 | - **SnackVideo**: `SnackVideoDownloader` 63 | - **Easy to Use** – Simple setup and intuitive usage. 64 | - **Vercel Deployment Support** – Deploy in seconds with a single click. 65 | 66 | --------------- 67 | ## This project can be run in three ways: 68 | 1. **Locally** (on your machine) 69 | 2. **On a VPS** (self-hosted) 70 | 3. **Deploying to Vercel** (serverless) 71 | 72 | --- 73 | 74 | ## 🖥️ Running Locally 75 | 76 | 1. **Clone the repository:** 77 | ```sh 78 | git clone https://github.com/000mistra000/selfhost-dl.git 79 | cd selfhost-dl 80 | ``` 81 | 82 | 2. **Install dependencies:** 83 | ```sh 84 | npm install 85 | ``` 86 | 87 | 3. **Start the server:** 88 | ```sh 89 | node index.js 90 | ``` 91 | 92 | 4. **Access the app:** 93 | ``` 94 | http://localhost: 95 | ``` 96 | 97 | --- 98 | 99 | ## 🌍 Running on a VPS 100 | 101 | ### Prerequisites: 102 | - A VPS with **Node.js (v16+)** installed 103 | - **PM2** (for process management) 104 | 105 | 1. **Connect to your VPS** via SSH: 106 | ```sh 107 | ssh user@your-vps-ip 108 | ``` 109 | 110 | 2. **Clone the repository:** 111 | ```sh 112 | git clone https://github.com/000mistra000/selfhost-dl.git 113 | cd selfhost-dl 114 | ``` 115 | 116 | 3. **Install dependencies:** 117 | ```sh 118 | npm install 119 | ``` 120 | 121 | 4. **Run with PM2 (recommended for production):** 122 | ```sh 123 | npm install -g pm2 124 | pm2 start index.js --name selfhost-dl 125 | pm2 save 126 | pm2 startup 127 | ``` 128 | 129 | 5. **Access the app via your VPS IP:** 130 | ``` 131 | http://your-vps-ip: 132 | ``` 133 | 134 | (Optional) **Use Nginx as a reverse proxy** for a custom domain and SSL setup. 135 | 136 | --- 137 | 138 | ## ☁️ Deploying to Vercel 139 | 140 | ### **One-Click Deploy** 141 | Click the button below to instantly deploy to **Vercel**: 142 | 143 |

144 | 145 | Deploy to Vercel 146 | 147 |

148 | 149 | --- 150 | ## License 151 | This project is licensed under the MIT License 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selfhost-dl", 3 | "version": "1.7.0", 4 | "description": "SelfHost-DL is my project that lets users self-host a simple web downloader with minimal setup. Just run it with Node.js—no third-party services needed. Deploy the files, start the server, and download content easily. Perfect for a lightweight, self-managed solution.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js", 9 | "vercel": "node vercel.js" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "axios": "^1.6.7", 14 | "cheerio": "^1.0.0-rc.12", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.4.1", 17 | "express": "^4.18.2", 18 | "soundcloud-downloader": "^1.0.0", 19 | "crypto": "^1.0.1" 20 | }, 21 | "devDependencies": { 22 | "nodemon": "^3.0.3" 23 | }, 24 | "engines": { 25 | "node": "18.x" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | .font-inter { 2 | font-family: 'Inter', sans-serif; 3 | } 4 | 5 | .font-poppins { 6 | font-family: 'Poppins', sans-serif; 7 | } 8 | 9 | .logo-animation { 10 | animation: float 3s ease-in-out infinite; 11 | } 12 | 13 | @keyframes float { 14 | 0% { transform: translateY(0px); } 15 | 50% { transform: translateY(-10px); } 16 | 100% { transform: translateY(0px); } 17 | } 18 | 19 | .loading-animation { 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | 25 | .loading-spinner { 26 | width: 48px; 27 | height: 48px; 28 | border: 4px solid var(--spinner-border-color, #f3f3f3); 29 | border-top: 4px solid var(--spinner-accent-color, #3b82f6); 30 | border-radius: 50%; 31 | animation: spin 1s linear infinite; 32 | } 33 | 34 | @keyframes spin { 35 | 0% { transform: rotate(0deg); } 36 | 100% { transform: rotate(360deg); } 37 | } 38 | 39 | .platform-card { 40 | @apply flex items-center gap-3 p-6 rounded-xl border transition-all duration-300 transform hover:scale-105 cursor-pointer; 41 | background: var(--card-bg, linear-gradient(to bottom right, #ffffff, #eef2ff)); 42 | border-color: var(--card-border-color, #e5e7eb); 43 | } 44 | 45 | .platform-card:hover { 46 | box-shadow: var(--card-hover-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1)); 47 | border-color: var(--card-hover-border-color, #818cf8); 48 | } 49 | 50 | .platform-card i { 51 | @apply transition-colors duration-300; 52 | color: var(--icon-color, #6366f1); 53 | } 54 | 55 | .platform-card:hover i { 56 | color: var(--icon-hover-color, #4f46e5); 57 | } 58 | 59 | .platform-card span { 60 | color: var(--text-color, #374151); 61 | font-weight: 500; 62 | } 63 | 64 | .download-item { 65 | @apply rounded-xl overflow-hidden transition-all duration-300; 66 | background: var(--item-bg, #ffffff); 67 | border: 1px solid var(--item-border-color, #e5e7eb); 68 | } 69 | 70 | .download-item:hover { 71 | box-shadow: var(--item-hover-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1)); 72 | } 73 | 74 | .download-stats { 75 | @apply grid grid-cols-2 md:grid-cols-4 gap-4 p-4 text-sm; 76 | background: var(--stats-bg, #f9fafb); 77 | color: var(--stats-text-color, #4b5563); 78 | } 79 | 80 | .stat-item { 81 | @apply flex items-center gap-2; 82 | } 83 | 84 | .download-button { 85 | @apply px-6 py-3 rounded-xl flex items-center gap-2 transform transition-all duration-300 86 | focus:outline-none focus:ring-2 focus:ring-offset-2; 87 | background: var(--button-bg, #6366f1); 88 | color: var(--button-text-color, #ffffff); 89 | } 90 | 91 | .download-button:hover { 92 | background: var(--button-hover-bg, #4f46e5); 93 | transform: scale(1.05); 94 | } 95 | 96 | .media-preview { 97 | @apply rounded-xl overflow-hidden; 98 | background: var(--preview-bg, #f9fafb); 99 | } 100 | 101 | .media-preview img, 102 | .media-preview video { 103 | @apply w-full h-auto object-cover; 104 | } 105 | 106 | .fade-in { 107 | animation: fadeIn 0.5s ease-out; 108 | } 109 | 110 | @keyframes fadeIn { 111 | from { 112 | opacity: 0; 113 | transform: translateY(10px); 114 | } 115 | to { 116 | opacity: 1; 117 | transform: translateY(0); 118 | } 119 | } 120 | 121 | @media (min-width: 768px) { 122 | * { 123 | scrollbar-width: auto; 124 | -ms-overflow-style: auto; 125 | } 126 | 127 | *::-webkit-scrollbar { 128 | display: block; 129 | } 130 | } 131 | 132 | @media (max-width: 767px) { 133 | * { 134 | scrollbar-width: auto; 135 | -ms-overflow-style: auto; 136 | } 137 | 138 | *::-webkit-scrollbar { 139 | display: block; 140 | } 141 | } 142 | 143 | @supports (scroll-behavior: smooth) { 144 | html { 145 | scroll-behavior: smooth; 146 | scroll-padding-top: 100px; 147 | } 148 | } 149 | 150 | section[id] { 151 | position: relative; 152 | scroll-margin-block-start: 100px; 153 | } 154 | 155 | body { 156 | overflow-x: hidden; 157 | width: 100%; 158 | } 159 | 160 | :root { 161 | --bg-gradient-light: linear-gradient(to bottom right, #f8fafc, #eef2ff); 162 | --bg-gradient-dark: linear-gradient(to bottom right, #0f172a, #1e293b); 163 | --main-bg: #ffffff; 164 | --text-primary: #1e293b; 165 | --text-secondary: #475569; 166 | --text-tertiary: #64748b; 167 | --card-bg: #ffffff; 168 | --card-hover: #f8fafc; 169 | --border-color: #e2e8f0; 170 | --icon-primary: #6366f1; 171 | --icon-secondary: #818cf8; 172 | --spinner-border-color: #f3f3f3; 173 | --spinner-accent-color: #3b82f6; 174 | --card-border-color: #e5e7eb; 175 | --card-hover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); 176 | --card-hover-border-color: #818cf8; 177 | --icon-color: #6366f1; 178 | --icon-hover-color: #4f46e5; 179 | --text-color: #374151; 180 | --item-bg: #ffffff; 181 | --item-border-color: #e5e7eb; 182 | --item-hover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); 183 | --stats-bg: #f9fafb; 184 | --stats-text-color: #4b5563; 185 | --button-bg: #6366f1; 186 | --button-text-color: #ffffff; 187 | --button-hover-bg: #4f46e5; 188 | --preview-bg: #f9fafb; 189 | --scrollbar-track: #f1f1f1; 190 | --scrollbar-thumb: #3b82f6; 191 | --scrollbar-thumb-hover: #2563eb; 192 | --faq-bg: #ffffff; 193 | --faq-hover: #f3f4f6; 194 | --faq-content-bg: #ffffff; 195 | --platform-item-bg: #f9fafb; 196 | --input-bg: #ffffff; 197 | --input-border: #e5e7eb; 198 | --input-text: #374151; 199 | --input-placeholder: #9ca3af; 200 | --gradient-from: #f8fafc; 201 | --gradient-to: #eef2ff; 202 | --issue-bg-from: #f9fafb; 203 | --issue-bg-to: #f3f4f6; 204 | --transition-accent: none; 205 | } 206 | 207 | .dark { 208 | --main-bg: #0f172a; 209 | --bg-gradient-light: linear-gradient(to bottom right, #0f172a, #1e293b); 210 | --text-primary: #c4c4c4; 211 | --text-secondary: #e2e8f0; 212 | --text-tertiary: #cbd5e1; 213 | --card-bg: #1e293b; 214 | --card-hover: #2d3748; 215 | --border-color: #374151; 216 | --icon-primary: #818cf8; 217 | --icon-secondary: #6366f1; 218 | --spinner-border-color: #374151; 219 | --spinner-accent-color: #6366f1; 220 | --card-border-color: #374151; 221 | --card-hover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); 222 | --card-hover-border-color: #6366f1; 223 | --icon-color: #818cf8; 224 | --icon-hover-color: #6366f1; 225 | --text-color: #e5e7eb; 226 | --item-bg: #1f2937; 227 | --item-border-color: #374151; 228 | --item-hover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); 229 | --stats-bg: #111827; 230 | --stats-text-color: #9ca3af; 231 | --button-bg: #6366f1; 232 | --button-text-color: #ffffff; 233 | --button-hover-bg: #4f46e5; 234 | --preview-bg: #111827; 235 | --scrollbar-track: #1f2937; 236 | --scrollbar-thumb: #6366f1; 237 | --scrollbar-thumb-hover: #818cf8; 238 | --faq-bg: #1f2937; 239 | --faq-hover: #2d3748; 240 | --faq-content-bg: #1f2937; 241 | --platform-item-bg: #1f2937; 242 | --input-bg: #374151; 243 | --input-border: #4b5563; 244 | --input-text: #e5e7eb; 245 | --input-placeholder: #9ca3af; 246 | --gradient-from: #0f172a; 247 | --gradient-to: #1e293b; 248 | --issue-bg-from: #1f2937; 249 | --issue-bg-to: #111827; 250 | } 251 | 252 | html { 253 | background-color: var(--main-bg); 254 | } 255 | 256 | body { 257 | background: var(--bg-gradient-light); 258 | color: var(--text-primary); 259 | min-height: 100vh; 260 | } 261 | 262 | .text-gray-800 { 263 | color: var(--text-primary) !important; 264 | } 265 | 266 | .text-gray-600 { 267 | color: var(--text-secondary) !important; 268 | } 269 | 270 | .border-gray-100, 271 | .border-gray-200 { 272 | border-color: var(--border-color) !important; 273 | } 274 | 275 | .text-indigo-500 { 276 | color: var(--icon-primary) !important; 277 | } 278 | 279 | .logo-bounce { 280 | animation: bounce 2s infinite; 281 | } 282 | 283 | @keyframes bounce { 284 | 0%, 100% { transform: translateY(0); } 285 | 50% { transform: translateY(-10px); } 286 | } 287 | 288 | * { 289 | transition: background-color 0.3s ease, 290 | color 0.3s ease, 291 | border-color 0.3s ease, 292 | box-shadow 0.3s ease, 293 | var(--transition-accent); 294 | } 295 | 296 | h1, h2, h3, h4, h5, h6 { 297 | color: var(--text-primary); 298 | } 299 | 300 | p { 301 | color: var(--text-secondary); 302 | } 303 | 304 | a { 305 | color: var(--text-secondary); 306 | } 307 | 308 | a:hover { 309 | color: var(--text-primary); 310 | } 311 | 312 | .faq-item button span { 313 | color: var(--text-primary); 314 | } 315 | 316 | .faq-content p { 317 | color: var(--text-secondary); 318 | } 319 | 320 | #platforms-list li span { 321 | color: var(--text-secondary); 322 | } 323 | 324 | input { 325 | background-color: var(--input-bg); 326 | border-color: var(--input-border); 327 | color: var(--input-text); 328 | } 329 | 330 | input::placeholder { 331 | color: var(--input-placeholder); 332 | } 333 | 334 | .faq-item button { 335 | background-color: var(--faq-bg); 336 | } 337 | 338 | .faq-item button:hover { 339 | background-color: var(--faq-hover); 340 | } 341 | 342 | .faq-content { 343 | background-color: var(--faq-content-bg); 344 | } 345 | 346 | #platforms-list li { 347 | @apply flex flex-col items-center p-2 rounded-lg border transition-all duration-300; 348 | background-color: var(--platform-item-bg); 349 | border-color: var(--border-color); 350 | } 351 | 352 | #platforms-list li:hover { 353 | @apply transform scale-105; 354 | background-color: var(--card-hover); 355 | border-color: var(--icon-primary); 356 | } 357 | 358 | .bg-white { 359 | background-color: var(--card-bg) !important; 360 | } 361 | 362 | .bg-gray-50 { 363 | background-color: var(--card-hover) !important; 364 | } 365 | 366 | @media (max-width: 640px) { 367 | .platform-card { 368 | @apply p-4; 369 | } 370 | 371 | .download-button { 372 | @apply px-4 py-2; 373 | } 374 | 375 | .loading-spinner { 376 | width: 36px; 377 | height: 36px; 378 | } 379 | } 380 | 381 | .bg-gradient-to-r { 382 | background: linear-gradient(to right, var(--issue-bg-from), var(--issue-bg-to)); 383 | } 384 | 385 | .dark .hover\:bg-gray-900:hover { 386 | background-color: var(--button-hover-dark); 387 | } 388 | 389 | .dark .hover\:bg-indigo-700:hover { 390 | background-color: var(--button-hover-primary); 391 | } 392 | 393 | .bg-gradient-to-r { 394 | transition: background 0.3s ease; 395 | } 396 | 397 | .faq-item { 398 | @apply transform transition-all duration-300; 399 | } 400 | 401 | .faq-toggle { 402 | width: 100%; 403 | background-color: var(--faq-bg); 404 | } 405 | 406 | .faq-toggle:hover { 407 | background-color: var(--faq-hover); 408 | } 409 | 410 | .faq-content { 411 | background-color: var(--faq-content-bg); 412 | } 413 | 414 | .faq-toggle, 415 | .faq-content, 416 | #platforms-list li { 417 | transition: all 0.3s ease; 418 | } 419 | 420 | .line-clamp-2 { 421 | display: -webkit-box; 422 | -webkit-line-clamp: 2; 423 | -webkit-box-orient: vertical; 424 | overflow: hidden; 425 | } 426 | 427 | .aspect-square { 428 | aspect-ratio: 1 / 1; 429 | } 430 | 431 | :root { 432 | --transition-accent: none; 433 | } 434 | 435 | :root { 436 | --red-50: #fef2f2; 437 | --red-100: #fee2e2; 438 | --red-200: #fecaca; 439 | --red-300: #fca5a5; 440 | --red-400: #f87171; 441 | --red-500: #ef4444; 442 | --red-600: #dc2626; 443 | --red-700: #b91c1c; 444 | --red-800: #991b1b; 445 | --red-900: #7f1d1d; 446 | 447 | --blue-50: #eff6ff; 448 | --blue-100: #dbeafe; 449 | --blue-200: #bfdbfe; 450 | --blue-300: #93c5fd; 451 | --blue-400: #60a5fa; 452 | --blue-500: #3b82f6; 453 | --blue-600: #2563eb; 454 | --blue-700: #1d4ed8; 455 | --blue-800: #1e40af; 456 | --blue-900: #1e3a8a; 457 | 458 | --green-50: #f0fdf4; 459 | --green-100: #dcfce7; 460 | --green-200: #bbf7d0; 461 | --green-300: #86efac; 462 | --green-400: #4ade80; 463 | --green-500: #22c55e; 464 | --green-600: #16a34a; 465 | --green-700: #15803d; 466 | --green-800: #166534; 467 | --green-900: #14532d; 468 | 469 | --yellow-50: #fefce8; 470 | --yellow-100: #fef9c3; 471 | --yellow-200: #fef08a; 472 | --yellow-300: #fde047; 473 | --yellow-400: #facc15; 474 | --yellow-500: #eab308; 475 | --yellow-600: #ca8a04; 476 | --yellow-700: #a16207; 477 | --yellow-800: #854d0e; 478 | --yellow-900: #713f12; 479 | 480 | --purple-50: #faf5ff; 481 | --purple-100: #f3e8ff; 482 | --purple-200: #e9d5ff; 483 | --purple-300: #d8b4fe; 484 | --purple-400: #c084fc; 485 | --purple-500: #a855f7; 486 | --purple-600: #9333ea; 487 | --purple-700: #7e22ce; 488 | --purple-800: #6b21a8; 489 | --purple-900: #581c87; 490 | 491 | --indigo-50: #eef2ff; 492 | --indigo-100: #e0e7ff; 493 | --indigo-200: #c7d2fe; 494 | --indigo-300: #a5b4fc; 495 | --indigo-400: #818cf8; 496 | --indigo-500: #6366f1; 497 | --indigo-600: #4f46e5; 498 | --indigo-700: #4338ca; 499 | --indigo-800: #3730a3; 500 | --indigo-900: #312e81; 501 | 502 | --pink-50: #fdf2f8; 503 | --pink-100: #fce7f3; 504 | --pink-200: #fbcfe8; 505 | --pink-300: #f9a8d4; 506 | --pink-400: #f472b6; 507 | --pink-500: #ec4899; 508 | --pink-600: #db2777; 509 | --pink-700: #be185d; 510 | --pink-800: #9d174d; 511 | --pink-900: #831843; 512 | } 513 | 514 | [data-accent="red"] { 515 | --accent-color: var(--red-500); 516 | --icon-primary: var(--red-500); 517 | --icon-secondary: var(--red-400); 518 | --button-bg: var(--red-500); 519 | --button-hover-bg: var(--red-600); 520 | --scrollbar-thumb: var(--red-500); 521 | --scrollbar-thumb-hover: var(--red-600); 522 | --spinner-accent-color: var(--red-500); 523 | --card-hover-border-color: var(--red-400); 524 | } 525 | 526 | [data-accent="blue"] { 527 | --accent-color: var(--blue-500); 528 | --icon-primary: var(--blue-500); 529 | --icon-secondary: var(--blue-400); 530 | --button-bg: var(--blue-500); 531 | --button-hover-bg: var(--blue-600); 532 | --scrollbar-thumb: var(--blue-500); 533 | --scrollbar-thumb-hover: var(--blue-600); 534 | --spinner-accent-color: var(--blue-500); 535 | --card-hover-border-color: var(--blue-400); 536 | } 537 | 538 | [data-accent="green"] { 539 | --accent-color: var(--green-500); 540 | --icon-primary: var(--green-500); 541 | --icon-secondary: var(--green-400); 542 | --button-bg: var(--green-500); 543 | --button-hover-bg: var(--green-600); 544 | --scrollbar-thumb: var(--green-500); 545 | --scrollbar-thumb-hover: var(--green-600); 546 | --spinner-accent-color: var(--green-500); 547 | --card-hover-border-color: var(--green-400); 548 | } 549 | 550 | 551 | * { 552 | transition: color 0.3s ease, 553 | background-color 0.3s ease, 554 | border-color 0.3s ease, 555 | box-shadow 0.3s ease; 556 | } 557 | 558 | .dark [data-accent] { 559 | --icon-primary: var(--accent-color); 560 | --icon-secondary: color-mix(in srgb, var(--accent-color) 80%, white); 561 | --button-bg: var(--accent-color); 562 | --button-hover-bg: color-mix(in srgb, var(--accent-color) 80%, black); 563 | --scrollbar-thumb: var(--accent-color); 564 | --scrollbar-thumb-hover: color-mix(in srgb, var(--accent-color) 80%, white); 565 | } 566 | 567 | .download-options { 568 | @apply fixed inset-0 z-50 flex items-center justify-center p-4; 569 | background-color: rgba(0, 0, 0, 0.5); 570 | backdrop-filter: blur(4px); 571 | } 572 | 573 | .download-popup { 574 | @apply w-full max-w-lg bg-white dark:bg-gray-800 rounded-2xl shadow-2xl overflow-hidden transform transition-all; 575 | animation: popup-enter 0.3s ease-out; 576 | } 577 | 578 | @keyframes popup-enter { 579 | from { 580 | opacity: 0; 581 | transform: scale(0.95) translateY(10px); 582 | } 583 | to { 584 | opacity: 1; 585 | transform: scale(1) translateY(0); 586 | } 587 | } 588 | 589 | .download-option { 590 | @apply flex items-center gap-4 p-4 transition-all duration-200 border-b border-gray-100 dark:border-gray-700/30; 591 | background: var(--item-bg); 592 | } 593 | 594 | .download-option:hover { 595 | @apply bg-gray-50 dark:bg-gray-700/30; 596 | transform: translateX(4px); 597 | } 598 | 599 | .download-option-icon { 600 | @apply w-12 h-12 rounded-xl flex items-center justify-center; 601 | background: var(--icon-bg, var(--gradient-from)); 602 | color: var(--icon-color); 603 | } 604 | 605 | .download-option-info { 606 | @apply flex-1; 607 | } 608 | 609 | .download-option-title { 610 | @apply text-sm font-medium; 611 | color: var(--text-primary); 612 | } 613 | 614 | .download-option-details { 615 | @apply text-xs mt-1; 616 | color: var(--text-secondary); 617 | } 618 | 619 | .download-option-action { 620 | @apply px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200; 621 | background: var(--button-bg); 622 | color: var(--button-text-color); 623 | } 624 | 625 | .download-option-action:hover { 626 | background: var(--button-hover-bg); 627 | transform: translateY(-1px); 628 | } 629 | 630 | .dark .download-popup { 631 | box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); 632 | } 633 | 634 | .dark .download-option-icon { 635 | background: var(--icon-bg, var(--gradient-to)); 636 | } 637 | 638 | section[id] { 639 | scroll-margin-top: 100px; 640 | } 641 | 642 | section[id]:target { 643 | animation: highlight 1s ease-out; 644 | } 645 | 646 | @keyframes highlight { 647 | from { 648 | outline: 2px solid transparent; 649 | } 650 | 50% { 651 | outline: 2px solid var(--accent-color); 652 | } 653 | to { 654 | outline: 2px solid transparent; 655 | } 656 | } 657 | 658 | section { 659 | padding-top: 2rem; 660 | padding-bottom: 2rem; 661 | } 662 | 663 | .fixed-header { 664 | position: sticky; 665 | top: 0; 666 | z-index: 50; 667 | } 668 | 669 | .media-preview-modal { 670 | @apply fixed inset-0 z-[999999] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 sm:p-8; 671 | } 672 | 673 | .media-preview-container { 674 | @apply relative w-full max-w-4xl bg-white/95 dark:bg-gray-800/95 backdrop-blur-lg rounded-2xl shadow-2xl overflow-hidden; 675 | border: 1px solid rgba(255, 255, 255, 0.1); 676 | } 677 | 678 | .media-preview-header { 679 | @apply flex items-center justify-between p-4 border-b border-gray-100/10 dark:border-gray-700/20; 680 | } 681 | 682 | .media-preview-content { 683 | @apply p-6; 684 | } 685 | 686 | .media-preview-video-container { 687 | @apply relative aspect-video rounded-xl overflow-hidden mb-6; 688 | background: rgba(0, 0, 0, 0.2); 689 | } 690 | 691 | .media-preview-stats { 692 | @apply grid grid-cols-3 gap-3 mb-6; 693 | } 694 | 695 | .media-preview-stat-item { 696 | @apply flex items-center gap-3 p-4 rounded-xl transition-all duration-300; 697 | background: rgba(var(--accent-rgb), 0.05); 698 | } 699 | 700 | .media-preview-downloads { 701 | @apply grid gap-3; 702 | } 703 | 704 | .media-preview-download-button { 705 | @apply flex items-center gap-3 p-4 rounded-xl transition-all duration-300; 706 | background: rgba(var(--accent-rgb), 0.05); 707 | } 708 | 709 | 710 | .media-preview-overlay { 711 | @apply fixed inset-0 z-[999999]; 712 | background: var(--modal-overlay-bg, rgba(0, 0, 0, 0.95)); 713 | backdrop-filter: blur(8px); 714 | } 715 | 716 | .media-preview-wrapper { 717 | @apply relative w-full max-w-md mx-4 my-4 rounded-2xl overflow-hidden shadow-2xl; 718 | background: var(--modal-bg, rgba(255, 255, 255, 0.02)); 719 | border: 1px solid var(--modal-border, rgba(255, 255, 255, 0.1)); 720 | } 721 | 722 | .media-preview-video-container { 723 | @apply relative bg-black; 724 | } 725 | 726 | .media-preview-info { 727 | background: var(--modal-content-bg, rgba(255, 255, 255, 0.02)); 728 | @apply p-4 space-y-4; 729 | } 730 | 731 | .media-preview-item { 732 | background: var(--modal-item-bg, rgba(255, 255, 255, 0.03)); 733 | border: 1px solid var(--modal-item-border, rgba(255, 255, 255, 0.1)); 734 | @apply rounded-xl p-3; 735 | } 736 | 737 | .media-preview-download-grid { 738 | @apply grid gap-2; 739 | } 740 | 741 | 742 | .dark { 743 | --modal-overlay-bg: rgba(0, 0, 0, 0.95); 744 | --modal-bg: rgba(17, 24, 39, 0.95); 745 | --modal-border: rgba(75, 85, 99, 0.3); 746 | --modal-content-bg: rgba(17, 24, 39, 0.95); 747 | --modal-item-bg: rgba(31, 41, 55, 0.4); 748 | --modal-item-border: rgba(75, 85, 99, 0.2); 749 | } 750 | 751 | 752 | :root { 753 | --modal-overlay-bg: rgba(0, 0, 0, 0.95); 754 | --modal-bg: rgba(255, 255, 255, 0.95); 755 | --modal-border: rgba(229, 231, 235, 0.2); 756 | --modal-content-bg: rgba(255, 255, 255, 0.95); 757 | --modal-item-bg: rgba(249, 250, 251, 0.4); 758 | --modal-item-border: rgba(229, 231, 235, 0.2); 759 | } 760 | 761 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 54 | 55 | 56 | 57 | All-in-One Media Downloader 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 |
76 | 77 |
78 | 187 | 188 |
189 |

191 | All-in-One Media Downloader 192 |

193 |

Download media from various social media platforms with ease.

194 |
195 |
196 | 197 |
198 |
200 |
201 |
202 |
203 | 204 |
205 | 209 |
210 | 211 | 219 | 220 | 225 |
226 | 227 | 240 |
241 | 242 |
244 |
245 |
246 |

About WebDL

247 |
248 | 249 |
250 |

251 | WebDL is a powerful and reliable downloader designed to support multiple platforms, making it easy for you to download content from various sources. With a focus on compatibility and efficiency, we ensure a seamless downloading experience across different services. 252 |

253 |
254 |
255 | 256 |
258 |
259 |

260 | 261 | Why Choose Us 262 |

263 |

Enjoy various benefits of our services

264 |
265 | 266 |
267 | 302 |
303 |
304 | 305 |
307 |
308 |
309 |

Supported Platforms

310 |
311 | 312 |
    324 | 334 |
335 |
336 | 337 | 338 |
340 |
341 |

342 | 343 | Frequently Asked Questions 344 |

345 | 346 |
359 | 387 |
388 |
389 | 390 | 404 |
405 |
406 |
407 | 408 | 409 | 418 | 419 | 447 | 530 | 605 | 606 |
614 | 615 |
616 |
617 |
618 |

619 | Download Options 620 |

621 | 626 |
627 |
628 | 629 |
630 | 651 |
652 |
653 |
654 | 655 | 656 | 666 | 667 | 668 | 678 | 679 | 680 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | async function downloadMedia() { 2 | const urlInput = document.getElementById('urlInput'); 3 | const result = document.getElementById('result'); 4 | const mediaPreview = document.getElementById('mediaPreview'); 5 | const savedColor = localStorage.getItem('accentColor') || 'indigo'; 6 | const downloadBtn = document.querySelector('.download-btn'); 7 | const originalBtnContent = downloadBtn.innerHTML; 8 | 9 | if (!urlInput.value.trim()) { 10 | mediaPreview.innerHTML = ` 11 |
12 |
13 |
14 | 15 |
16 |

URL cannot be empty

17 |
18 |
19 | `; 20 | result.classList.remove('hidden'); 21 | return; 22 | } 23 | 24 | try { 25 | result.classList.add('hidden'); 26 | mediaPreview.innerHTML = ''; 27 | 28 | 29 | downloadBtn.disabled = true; 30 | downloadBtn.innerHTML = ` 31 |
32 |
33 | Processing... 34 |
35 | `; 36 | 37 | const response = await fetch('/api/download', { 38 | method: 'POST', 39 | headers: { 'Content-Type': 'application/json' }, 40 | body: JSON.stringify({ url: urlInput.value }) 41 | }); 42 | 43 | const data = await response.json(); 44 | 45 | if (data.error) throw new Error(data.error); 46 | 47 | window.lastMediaData = data; 48 | mediaPreview.innerHTML = generateMediaPreviewHTML(data, savedColor); 49 | result.classList.remove('hidden'); 50 | } catch (error) { 51 | console.error('Error:', error); 52 | showError(error.message); 53 | } finally { 54 | 55 | downloadBtn.disabled = false; 56 | downloadBtn.innerHTML = originalBtnContent; 57 | } 58 | } 59 | 60 | function showMediaPreview(data) { 61 | const videoUrl = data.downloads.find(d => d.type.includes('video'))?.url; 62 | if (!videoUrl) return; 63 | 64 | const savedColor = localStorage.getItem('accentColor') || 'indigo'; 65 | const modal = document.createElement('div'); 66 | modal.className = 'fixed inset-0 z-[999999] flex items-start justify-center overflow-y-auto'; 67 | modal.style.cssText = 'margin-top: max(env(safe-area-inset-top), 1rem);'; 68 | 69 | modal.innerHTML = ` 70 |
71 |
72 | 73 |
74 |
75 | 82 |
83 | 84 | 85 | 89 |
90 | 91 | 92 |
93 | 94 | ${data.caption ? ` 95 |
96 |

97 | ${data.caption} 98 |

99 |
100 | ` : ''} 101 | 102 | 103 | ${data.author ? ` 104 |
105 |
106 | 107 |
108 |
109 |
${data.author}
110 | ${data.username ? `
@${data.username}
` : ''} 111 |
112 |
113 | ` : ''} 114 | 115 | 116 |
117 | ${['views', 'like', 'comments'].map(stat => 118 | data[stat] ? ` 119 |
120 |
121 | 122 |
123 |
124 | ${formatNumber(data[stat])} 125 | ${stat} 126 |
127 |
128 | ` : '' 129 | ).filter(Boolean).join('')} 130 |
131 | 132 | 133 |
134 | ${data.downloads 135 | .sort((a, b) => { 136 | const qualityOrder = { 137 | '2160p': 1, '1440p': 2, '1080p': 3, '720p': 4, 138 | '480p': 5, '360p': 6, '240p': 7 139 | }; 140 | return (qualityOrder[a.quality] || 99) - (qualityOrder[b.quality] || 99); 141 | }) 142 | .map(download => ` 143 | 146 |
147 |
148 | 149 |
150 |
151 | ${getDisplayName(download.type)} 152 | Klik untuk buka 153 |
154 |
155 |
156 | `).join('')} 157 |
158 |
159 |
160 | `; 161 | 162 | document.body.appendChild(modal); 163 | 164 | const videoPlayer = modal.querySelector('video'); 165 | 166 | 167 | videoPlayer.addEventListener('click', (e) => { 168 | e.stopPropagation(); 169 | if (videoPlayer.paused) { 170 | videoPlayer.play(); 171 | } else { 172 | videoPlayer.pause(); 173 | } 174 | }); 175 | 176 | 177 | videoPlayer.addEventListener('loadedmetadata', () => { 178 | videoPlayer.play().catch(error => { 179 | console.log('Auto-play prevented:', error); 180 | const playButton = document.createElement('button'); 181 | playButton.className = 'absolute inset-0 w-full h-full flex items-center justify-center bg-black/50'; 182 | playButton.innerHTML = ` 183 |
184 | 185 |
186 | `; 187 | playButton.onclick = (e) => { 188 | e.stopPropagation(); 189 | videoPlayer.play(); 190 | playButton.remove(); 191 | }; 192 | videoPlayer.parentElement.appendChild(playButton); 193 | }); 194 | }); 195 | 196 | 197 | videoPlayer.addEventListener('error', () => { 198 | const errorMessage = document.createElement('div'); 199 | errorMessage.className = 'absolute inset-0 flex items-center justify-center bg-black'; 200 | errorMessage.innerHTML = ` 201 |
202 | 203 |

Video tidak dapat diputar

204 |

Coba refresh halaman atau gunakan browser lain

205 |
206 | `; 207 | videoPlayer.parentElement.appendChild(errorMessage); 208 | }); 209 | 210 | 211 | modal.addEventListener('click', (e) => { 212 | if (e.target === modal) { 213 | closeMediaPreview(modal.querySelector('button')); 214 | } 215 | }); 216 | 217 | 218 | const handleKeyPress = (e) => { 219 | if (e.key === 'Escape') { 220 | closeMediaPreview(modal.querySelector('button')); 221 | } else if (e.key === ' ') { 222 | e.preventDefault(); 223 | if (videoPlayer.paused) { 224 | videoPlayer.play(); 225 | } else { 226 | videoPlayer.pause(); 227 | } 228 | } 229 | }; 230 | 231 | document.addEventListener('keydown', handleKeyPress); 232 | modal.handleKeyPress = handleKeyPress; 233 | } 234 | 235 | function toggleDownloadOptions(button) { 236 | const downloadPanel = button.closest('.relative').querySelector('#downloadOptions'); 237 | downloadPanel.classList.toggle('hidden'); 238 | } 239 | 240 | function closeMediaPreview(element) { 241 | const modal = element.closest('.fixed'); 242 | if (!modal) return; 243 | 244 | const video = modal.querySelector('video'); 245 | if (video) { 246 | video.pause(); 247 | video.src = ''; 248 | video.load(); 249 | } 250 | 251 | 252 | if (modal.handleKeyPress) { 253 | document.removeEventListener('keydown', modal.handleKeyPress); 254 | } 255 | 256 | modal.remove(); 257 | } 258 | 259 | async function showDownloadOptions(data) { 260 | try { 261 | const savedColor = localStorage.getItem('accentColor') || 'indigo'; 262 | const downloadData = typeof data === 'string' ? JSON.parse(data) : data; 263 | 264 | const modal = document.createElement('div'); 265 | modal.setAttribute('x-data', ''); 266 | modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4'; 267 | modal.innerHTML = ` 268 |
269 |
270 |
271 |

272 | ${downloadData.metadata?.title || 'Download Options'} 273 |

274 | 278 |
279 |
280 | ${downloadData.downloads.map(download => ` 281 | 300 | `).join('')} 301 |
302 |
303 | `; 304 | 305 | document.body.appendChild(modal); 306 | Alpine.initTree(modal); 307 | 308 | const backdrop = modal.querySelector('.absolute'); 309 | const content = modal.querySelector('.relative'); 310 | 311 | requestAnimationFrame(() => { 312 | backdrop.classList.add('opacity-100'); 313 | content.classList.add('scale-100', 'opacity-100'); 314 | }); 315 | 316 | const closeHandler = (e) => { 317 | if (e.target === modal) { 318 | closeModal(modal); 319 | } 320 | }; 321 | 322 | const escHandler = (e) => { 323 | if (e.key === 'Escape') { 324 | closeModal(modal); 325 | } 326 | }; 327 | 328 | modal.addEventListener('click', closeHandler); 329 | document.addEventListener('keydown', escHandler); 330 | 331 | modal.closeHandler = closeHandler; 332 | modal.escHandler = escHandler; 333 | 334 | } catch (error) { 335 | console.error('Error showing download options:', error); 336 | showError('Gagal menampilkan opsi download'); 337 | } 338 | } 339 | 340 | function closeModal(modal) { 341 | if (!modal) return; 342 | 343 | const backdrop = modal.querySelector('.absolute'); 344 | const content = modal.querySelector('.relative'); 345 | 346 | backdrop.classList.remove('opacity-100'); 347 | content.classList.remove('scale-100', 'opacity-100'); 348 | content.classList.add('scale-95', 'opacity-0'); 349 | 350 | if (modal.closeHandler) { 351 | modal.removeEventListener('click', modal.closeHandler); 352 | } 353 | if (modal.escHandler) { 354 | document.removeEventListener('keydown', modal.escHandler); 355 | } 356 | 357 | setTimeout(() => { 358 | modal.remove(); 359 | }, 300); 360 | } 361 | 362 | function getIconForType(type) { 363 | const icons = { 364 | download_video_hd: 'fa-film', 365 | download_video_2160p: 'fa-film', 366 | download_video_1440p: 'fa-film', 367 | download_video_1080p: 'fa-film', 368 | download_video_720p: 'fa-film', 369 | download_video_480p: 'fa-video', 370 | download_video_360p: 'fa-video', 371 | download_video_240p: 'fa-video', 372 | download_audio: 'fa-music', 373 | download_image: 'fa-image' 374 | }; 375 | return icons[type] || 'fa-download'; 376 | } 377 | 378 | function getDisplayName(type) { 379 | const names = { 380 | download_video_hd: 'HD Quality', 381 | download_video_2160p: '4K Ultra HD', 382 | download_video_1440p: '2K Quality', 383 | download_video_1080p: 'Full HD', 384 | download_video_720p: 'HD 720p', 385 | download_video_480p: 'SD 480p', 386 | download_video_360p: 'Low 360p', 387 | download_video_240p: 'Low 240p', 388 | download_audio: 'Audio MP3', 389 | download_image: 'Image HD' 390 | }; 391 | return names[type] || type; 392 | } 393 | 394 | function getIconForStat(stat) { 395 | const icons = { 396 | 'views': 'eye', 397 | 'like': 'heart', 398 | 'comments': 'comment' 399 | }; 400 | return icons[stat] || 'chart-bar'; 401 | } 402 | 403 | function formatNumber(num) { 404 | if (num >= 1000000) { 405 | return (num / 1000000).toFixed(1) + 'M'; 406 | } 407 | if (num >= 1000) { 408 | return (num / 1000).toFixed(1) + 'K'; 409 | } 410 | return num.toString(); 411 | } 412 | 413 | function showError(message) { 414 | const mediaPreview = document.getElementById('mediaPreview'); 415 | mediaPreview.innerHTML = ` 416 |
417 |
418 |
419 | 420 |
421 |

${message}

422 |
423 |
424 | `; 425 | } 426 | 427 | function initColorManager() { 428 | const availableColors = [ 429 | { name: 'red', label: 'Merah' }, 430 | { name: 'blue', label: 'Biru' }, 431 | { name: 'green', label: 'Hijau' }, 432 | { name: 'yellow', label: 'Kuning' }, 433 | { name: 'purple', label: 'Ungu' }, 434 | { name: 'indigo', label: 'Indigo' }, 435 | { name: 'pink', label: 'Pink' }, 436 | ]; 437 | 438 | const currentColor = localStorage.getItem('accentColor') || 'indigo'; 439 | 440 | document.documentElement.setAttribute('data-accent', currentColor); 441 | 442 | const menuDropdown = document.querySelector('.dropdown-menu'); 443 | if (menuDropdown) { 444 | menuDropdown.style.zIndex = '99999'; 445 | } 446 | 447 | const colorSection = document.createElement('div'); 448 | colorSection.className = 'px-4 py-3 border-t border-gray-100 dark:border-gray-700/30'; 449 | 450 | colorSection.innerHTML = ` 451 |
452 | Accent Color 453 |
454 |
455 | ${availableColors.map(color => ` 456 | 461 | `).join('')} 462 |
463 | `; 464 | 465 | colorSection.querySelectorAll('button[data-color]').forEach(button => { 466 | button.addEventListener('click', () => { 467 | const newColor = button.dataset.color; 468 | setAccentColor(newColor); 469 | 470 | updateDynamicElements(newColor); 471 | window.dispatchEvent(new CustomEvent('accent-color-changed', { 472 | detail: { oldColor, newColor } 473 | })); 474 | }); 475 | }); 476 | 477 | const darkModeSection = document.querySelector('.dark-mode-section'); 478 | darkModeSection.parentNode.insertBefore(colorSection, darkModeSection.nextSibling); 479 | } 480 | 481 | document.addEventListener('DOMContentLoaded', initColorManager); 482 | 483 | function setAccentColor(newColor, oldColor = 'indigo') { 484 | 485 | localStorage.setItem('accentColor', newColor); 486 | 487 | 488 | if (window.Alpine) { 489 | Alpine.store('accent', { 490 | color: newColor 491 | }); 492 | } 493 | 494 | 495 | const elements = document.querySelectorAll('*'); 496 | elements.forEach(element => { 497 | 498 | if (element.classList && element.classList.length > 0) { 499 | const classList = Array.from(element.classList); 500 | 501 | const prefixes = [ 502 | 'bg', 'text', 'border', 'ring', 'from', 'to', 'via', 503 | 'hover:bg', 'hover:text', 'hover:border', 'hover:ring', 504 | 'focus:bg', 'focus:text', 'focus:border', 'focus:ring', 505 | 'active:bg', 'active:text', 'active:border', 506 | 'dark:bg', 'dark:text', 'dark:border', 'dark:ring', 507 | 'dark:hover:bg', 'dark:hover:text', 'dark:hover:border', 508 | 'dark:focus:bg', 'dark:focus:text', 'dark:focus:border', 509 | 'shadow', 'group-hover' 510 | ]; 511 | 512 | const intensities = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900']; 513 | 514 | prefixes.forEach(prefix => { 515 | intensities.forEach(intensity => { 516 | const oldClass = `${prefix}-${oldColor}-${intensity}`; 517 | const newClass = `${prefix}-${newColor}-${intensity}`; 518 | 519 | if (classList.includes(oldClass)) { 520 | element.classList.remove(oldClass); 521 | element.classList.add(newClass); 522 | } 523 | }); 524 | }); 525 | } 526 | 527 | 528 | if (element.style) { 529 | const styleProps = ['backgroundColor', 'color', 'borderColor']; 530 | styleProps.forEach(prop => { 531 | if (element.style[prop]?.includes(oldColor)) { 532 | element.style[prop] = element.style[prop].replace(oldColor, newColor); 533 | } 534 | }); 535 | } 536 | }); 537 | 538 | 539 | window.dispatchEvent(new CustomEvent('accent-color-changed', { 540 | detail: { oldColor, newColor } 541 | })); 542 | 543 | 544 | document.querySelectorAll('[x-data]').forEach(el => { 545 | if (el.__x) { 546 | el.__x.$data.currentColor = newColor; 547 | } 548 | }); 549 | } 550 | 551 | 552 | document.addEventListener('alpine:init', () => { 553 | Alpine.store('accent', { 554 | color: localStorage.getItem('accentColor') || 'indigo', 555 | 556 | setColor(newColor) { 557 | const oldColor = this.color; 558 | this.color = newColor; 559 | setAccentColor(newColor, oldColor); 560 | } 561 | }); 562 | }); 563 | 564 | function updateDynamicElements(color) { 565 | const spinner = document.querySelector('.loading-spinner'); 566 | if (spinner) { 567 | spinner.innerHTML = ` 568 |
569 |
570 |
571 |
572 |
573 |
574 | `; 575 | } 576 | 577 | if (window.lastMediaData) { 578 | const mediaPreview = document.getElementById('mediaPreview'); 579 | if (mediaPreview) { 580 | mediaPreview.innerHTML = generateMediaPreviewHTML(window.lastMediaData, color); 581 | } 582 | } 583 | 584 | const gradients = document.querySelectorAll('[class*="gradient"]'); 585 | gradients.forEach(gradient => { 586 | gradient.className = gradient.className 587 | .replace(/from-\w+-\d+/g, `from-${color}-50`) 588 | .replace(/to-\w+-\d+/g, `to-${color}-50`); 589 | }); 590 | 591 | document.querySelectorAll('button, a').forEach(element => { 592 | if (element.className.includes('bg-') || element.className.includes('text-')) { 593 | updateElementColors(element, color); 594 | } 595 | }); 596 | } 597 | 598 | function updateElementColors(element, newColor, oldColor = 'indigo') { 599 | const classList = Array.from(element.classList); 600 | 601 | classList.forEach(className => { 602 | if (className.includes(oldColor) || className.includes('indigo')) { 603 | const newClass = className 604 | .replace(oldColor, newColor) 605 | .replace('indigo', newColor); 606 | element.classList.remove(className); 607 | element.classList.add(newClass); 608 | } 609 | }); 610 | } 611 | 612 | const observer = new MutationObserver((mutations) => { 613 | const currentColor = localStorage.getItem('accentColor') || 'indigo'; 614 | mutations.forEach(mutation => { 615 | mutation.addedNodes.forEach(node => { 616 | if (node.nodeType === 1) { if (node.classList.contains('dropdown-menu')) { 617 | node.style.zIndex = '99999'; 618 | } else if (node.classList.contains('modal')) { 619 | node.style.zIndex = '99999'; 620 | } else { 621 | node.style.zIndex = '0'; 622 | } 623 | 624 | updateElementColors(node, currentColor); 625 | node.querySelectorAll('*').forEach(child => { 626 | updateElementColors(child, currentColor); 627 | }); 628 | } 629 | }); 630 | }); 631 | }); 632 | 633 | observer.observe(document.body, { 634 | childList: true, 635 | subtree: true, 636 | attributes: true, 637 | attributeFilter: ['class'] 638 | }); 639 | 640 | document.addEventListener('DOMContentLoaded', () => { 641 | const savedColor = localStorage.getItem('accentColor') || 'indigo'; 642 | if (savedColor !== 'indigo') { 643 | setAccentColor(savedColor, 'indigo'); 644 | } 645 | }); 646 | 647 | function generateMediaPreviewHTML(data, savedColor) { 648 | return ` 649 |
650 |
651 | 652 |
653 |
655 | ${data['img-thumb'] ? ` 656 | Preview 660 | ` : ` 661 |
662 | 663 |
664 | `} 665 |
666 |
667 | 668 |
669 |
670 |
671 |
672 | 673 | 674 |
675 | 676 | ${data.platform ? ` 677 |
678 |
Detected Platform
679 |
680 | ${getPlatformBadges(data.platform, savedColor)} 681 |
682 |
683 | ` : ''} 684 | 685 | 686 | 691 |
692 |
693 |
694 | `; 695 | } 696 | 697 | 698 | function getPlatformBadges(platform, savedColor) { 699 | const platforms = { 700 | 'tiktok': { icon: 'fab fa-tiktok', label: 'TikTok' }, 701 | 'instagram': { icon: 'fab fa-instagram', label: 'Instagram' }, 702 | 'youtube': { icon: 'fab fa-youtube', label: 'YouTube' }, 703 | 'facebook': { icon: 'fab fa-facebook', label: 'Facebook' }, 704 | 'capcut': { icon: 'fas fa-video', label: 'CapCut' }, 705 | 'rednote': { icon: 'fas fa-book', label: 'RedNote' }, 706 | 'threads': { icon: 'fab fa-at', label: 'Threads' }, 707 | 'soundcloud': { icon: 'fab fa-soundcloud', label: 'Soundcloud' }, 708 | 'spotify': { icon: 'fab fa-spotify', label: 'Spotify' }, 709 | 'terabox': { icon: 'fas fa-box', label: 'Terabox' }, 710 | 'snackvideo': { icon: 'fas fa-video', label: 'Snackvideo' }, 711 | 'doodstream': { icon: 'fas fa-video', label: 'Doodstream' } 712 | }; 713 | 714 | const platformInfo = platforms[platform?.toLowerCase()] || { icon: 'fas fa-link', label: platform || 'Unknown' }; 715 | 716 | return ` 717 |
718 | 719 | ${platformInfo.label} 720 |
721 | `; 722 | } 723 | 724 | window.addEventListener('accent-color-changed', (event) => { 725 | const { oldColor, newColor } = event.detail; 726 | updateAccentColor(document.body, oldColor, newColor); 727 | }); 728 | 729 | function updateAccentColor(element, oldColor, newColor) { 730 | updateElementColors(element, newColor, oldColor); 731 | 732 | element.childNodes.forEach(child => { 733 | if (child.nodeType === 1) { updateAccentColor(child, oldColor, newColor); 734 | } 735 | }); 736 | } 737 | 738 | function downloadFile(url) { 739 | if (!url) return; 740 | window.open(url, '_blank'); 741 | } 742 | 743 | function showToast(message, type = 'info') { 744 | const savedColor = localStorage.getItem('accentColor') || 'indigo'; 745 | const toast = document.createElement('div'); 746 | toast.className = `fixed bottom-4 left-4 right-4 sm:left-auto sm:right-4 px-4 sm:px-6 py-3 rounded-xl shadow-lg text-white transform transition-all duration-300 z-50 text-sm sm:text-base text-center sm:text-left ${ 747 | type === 'error' ? 'bg-red-500' : `bg-${savedColor}-500` 748 | }`; 749 | toast.textContent = message; 750 | document.body.appendChild(toast); 751 | 752 | setTimeout(() => { 753 | toast.style.opacity = '0'; 754 | setTimeout(() => toast.remove(), 300); 755 | }, 3000); 756 | } 757 | 758 | function getFileExtension(type) { 759 | const extensions = { 760 | video_2160p: 'mp4', 761 | video_1440p: 'mp4', 762 | video_hd: 'mp4', 763 | video_watermark: 'mp4', 764 | audio: 'mp3', 765 | image: 'jpg', 766 | webp: 'webp' 767 | }; 768 | return extensions[type] || 'mp4'; 769 | } 770 | 771 | document.querySelectorAll('a[href^="#"]').forEach(anchor => { 772 | anchor.addEventListener('click', function (e) { 773 | e.preventDefault(); 774 | const targetId = this.getAttribute('href'); 775 | const targetElement = document.querySelector(targetId); 776 | 777 | if (targetElement) { 778 | targetElement.scrollIntoView({ 779 | behavior: 'smooth', 780 | block: 'center' 781 | }); 782 | 783 | history.pushState(null, '', targetId); 784 | } 785 | }); 786 | }); 787 | 788 | window.addEventListener('scroll', () => { 789 | const sections = document.querySelectorAll('section[id]'); 790 | const scrollY = window.scrollY || document.documentElement.scrollTop; 791 | 792 | sections.forEach(section => { 793 | const sectionHeight = section.offsetHeight; 794 | const sectionTop = section.offsetTop - 100; 795 | const sectionId = section.getAttribute('id'); 796 | 797 | if (scrollY > sectionTop && scrollY <= sectionTop + sectionHeight) { 798 | document.querySelector(`a[href="#${sectionId}"]`)?.classList.add('active'); 799 | } else { 800 | document.querySelector(`a[href="#${sectionId}"]`)?.classList.remove('active'); 801 | } 802 | }); 803 | }); 804 | 805 | 806 | const style = document.createElement('style'); 807 | style.textContent = ` 808 | @keyframes custom-spin { 809 | 0% { transform: rotate(0deg); } 810 | 100% { transform: rotate(360deg); } 811 | } 812 | 813 | .animate-spin { 814 | animation: custom-spin 0.8s linear infinite; 815 | } 816 | `; 817 | document.head.appendChild(style); 818 | document.head.appendChild(style); 819 | 820 | 821 | function updateLoadingSpinner() { 822 | const savedColor = localStorage.getItem('accentColor') || 'indigo'; 823 | const loading = document.getElementById('loading'); 824 | if (loading) { 825 | loading.innerHTML = ` 826 |
827 |
828 |
829 |
830 |
831 |
832 | `; 833 | } 834 | } 835 | 836 | 837 | window.addEventListener('accent-color-changed', updateLoadingSpinner); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const path = require('path'); 4 | const crypto = require('crypto').webcrypto; 5 | global.crypto = crypto; 6 | const Downloader = require('./src/downloader'); 7 | const app = express(); 8 | const PORT = process.env.PORT || 3996; // PORT 9 | app.use(cors()); 10 | app.use(express.json()); 11 | app.use(express.static('public')); 12 | app.get('/', (_, res) => { 13 | res.sendFile(path.join(__dirname, 'public', 'index.html')); 14 | }); 15 | 16 | app.post('/api/download', async (req, res) => { 17 | try { 18 | const { url } = req.body; 19 | 20 | if (!url) { 21 | return res.status(400).json({ 22 | error: 'URL cannot be empty' 23 | }); 24 | } 25 | 26 | const downloader = new Downloader(); 27 | const platform = downloader.getPlatform(url); 28 | 29 | if (!platform) { 30 | return res.status(400).json({ 31 | error: 'Platform not supported' 32 | }); 33 | } 34 | 35 | const result = await downloader.download(url); 36 | 37 | if (!result) { 38 | return res.status(404).json({ 39 | error: 'Media not found' 40 | }); 41 | } 42 | 43 | res.json(result); 44 | 45 | } catch (error) { 46 | console.error('Download Error:', error); 47 | res.status(500).json({ 48 | error: error.message || 'Error occurred while downloading media' 49 | }); 50 | } 51 | }); 52 | 53 | 54 | app.use((err, _, res, _next) => { 55 | console.error('Server Error:', err); 56 | res.status(500).json({ 57 | error: 'Internal server error' 58 | }); 59 | }); 60 | 61 | 62 | app.use((_, res) => { 63 | res.status(404).json({ 64 | error: 'Endpoint not found' 65 | }); 66 | }); 67 | 68 | 69 | app.listen(PORT, () => { 70 | console.log(`Server running on port ${PORT}`); 71 | console.log(`URL: http://localhost:${PORT}`); 72 | 73 | 74 | const downloader = new Downloader(); 75 | const platforms = Object.keys(downloader.platformPatterns); 76 | console.log('\nSupported platforms:'); 77 | platforms.forEach(platform => { 78 | console.log(`- ${platform}`); 79 | }); 80 | }); 81 | 82 | 83 | process.on('uncaughtException', (err) => { 84 | console.error('Uncaught Exception:', err); 85 | process.exit(1); 86 | }); 87 | 88 | 89 | process.on('unhandledRejection', (err) => { 90 | console.error('Unhandled Rejection:', err); 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /src/downloader.js: -------------------------------------------------------------------------------- 1 | const TiktokDownloader = require('./scrape/tiktok'); 2 | const CapcutDownloader = require('./scrape/capcut'); 3 | const XiaohongshuDownloader = require('./scrape/xiaohongshu'); 4 | const ThreadsDownloader = require('./scrape/threads'); 5 | const SoundcloudDownloader = require('./scrape/soundcloud'); 6 | const SpotifyDownloader = require('./scrape/spotify'); 7 | const InstagramDownloader = require('./scrape/instagram'); 8 | const FacebookDownloader = require('./scrape/facebook'); 9 | const TeraboxDownloader = require('./scrape/terabox'); 10 | const SnackVideoDownloader = require('./scrape/snackvideo'); 11 | const platformPatterns = require('./system/patterns.js'); 12 | 13 | class Downloader { 14 | constructor() { 15 | this.platformPatterns = platformPatterns; 16 | this.downloaders = { 17 | tiktok: TiktokDownloader, 18 | capcut: CapcutDownloader, 19 | xiaohongshu: XiaohongshuDownloader, 20 | threads: ThreadsDownloader, 21 | soundcloud: SoundcloudDownloader, 22 | spotify: SpotifyDownloader, 23 | instagram: InstagramDownloader, 24 | facebook: FacebookDownloader, 25 | terabox: TeraboxDownloader, 26 | snackvideo: SnackVideoDownloader 27 | }; 28 | } 29 | 30 | getPlatform(url) { 31 | if (!url) throw new Error('URL tidak boleh kosong'); 32 | 33 | for (const [platform, pattern] of Object.entries(this.platformPatterns)) { 34 | if (pattern.test(url)) { 35 | return platform; 36 | } 37 | } 38 | 39 | throw new Error('Platform tidak didukung'); 40 | } 41 | 42 | async download(url) { 43 | try { 44 | if (!url) { 45 | throw new Error('URL tidak boleh kosong'); 46 | } 47 | 48 | url = url.trim(); 49 | 50 | if (!/^https?:\/\//i.test(url)) { 51 | url = 'https://' + url; 52 | } 53 | 54 | const platform = this.getPlatform(url); 55 | 56 | if (!this.downloaders[platform]) { 57 | throw new Error(`Platform ${platform} tidak didukung`); 58 | } 59 | 60 | const DownloaderClass = this.downloaders[platform]; 61 | const downloader = new DownloaderClass(url); 62 | const result = await downloader.download(); 63 | 64 | if (!result) { 65 | throw new Error('Gagal mendapatkan hasil download'); 66 | } 67 | 68 | return result; 69 | 70 | } catch (error) { 71 | console.error('Download Error:', error); 72 | throw error; 73 | } 74 | } 75 | } 76 | 77 | module.exports = Downloader; 78 | -------------------------------------------------------------------------------- /src/scrape/capcut.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | 4 | class CapcutDownloader { 5 | constructor(url) { 6 | this.url = url; 7 | } 8 | 9 | async download() { 10 | try { 11 | if (!this.url.includes('capcut.com')) { 12 | throw new Error('URL CapCut tidak valid'); 13 | } 14 | 15 | const headers = { 16 | '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', 17 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 18 | 'Accept-Language': 'en-US,en;q=0.5', 19 | 'Referer': 'https://www.capcut.com/' 20 | }; 21 | 22 | const response = await axios.get(this.url, { headers }); 23 | const $ = cheerio.load(response.data); 24 | const videoName = $("img").attr("alt") || 'CapCut Video'; 25 | const thumbnail = $("img").attr("src"); 26 | const videoUrl = $("video").attr("src"); 27 | 28 | if (!videoUrl) { 29 | throw new Error('Tidak dapat menemukan URL video'); 30 | } 31 | 32 | const timestamp = Date.now(); 33 | const safeFileName = videoName 34 | .replace(/[^a-z0-9]/gi, '_') 35 | .toLowerCase(); 36 | 37 | return { 38 | platform: 'capcut', 39 | caption: videoName, 40 | author: '', 41 | username: '', 42 | 'img-thumb': thumbnail, 43 | like: 0, 44 | views: 0, 45 | comments: 0, 46 | date: new Date().toLocaleString('id-ID', { 47 | day: 'numeric', 48 | month: 'long', 49 | year: 'numeric', 50 | hour: '2-digit', 51 | minute: '2-digit' 52 | }), 53 | downloads: [{ 54 | type: 'download_video_hd', 55 | url: videoUrl, 56 | filename: `capcut_${safeFileName}_${timestamp}.mp4` 57 | }] 58 | }; 59 | 60 | } catch (error) { 61 | console.error('CapCut Download Error:', error); 62 | if (error.response) { 63 | console.error('Response Error:', error.response.data); 64 | } 65 | throw new Error('Gagal mengunduh dari CapCut: ' + error.message); 66 | } 67 | } 68 | } 69 | 70 | module.exports = CapcutDownloader; 71 | -------------------------------------------------------------------------------- /src/scrape/facebook.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const patterns = require("../system/patterns"); 3 | 4 | class FacebookDownloader { 5 | constructor(url) { 6 | this.url = url; 7 | if (!patterns.facebook.test(url)) { 8 | throw new Error("invalid url. please enter a valid facebook url"); 9 | } 10 | } 11 | 12 | async download() { 13 | try { 14 | const results = await this.getFacebookData(this.url); 15 | if (!results || !results.downloads || results.downloads.length === 0) { 16 | throw new Error("no media found"); 17 | } 18 | return results; 19 | } catch (error) { 20 | throw new Error('error while downloading from facebook: ' + error.message); 21 | } 22 | } 23 | 24 | async getFacebookData(url) { 25 | if (!url.includes('facebook.com')) { 26 | url = `https://www.facebook.com/${url}`; 27 | } 28 | 29 | try { 30 | const headers = { 31 | "sec-fetch-user": "?1", 32 | "sec-ch-ua-mobile": "?0", 33 | "sec-fetch-site": "none", 34 | "sec-fetch-dest": "document", 35 | "sec-fetch-mode": "navigate", 36 | "cache-control": "max-age=0", 37 | authority: "www.facebook.com", 38 | "upgrade-insecure-requests": "1", 39 | "accept-language": "en-GB,en;q=0.9,tr-TR;q=0.8,tr;q=0.7,en-US;q=0.6", 40 | "sec-ch-ua": '"Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"', 41 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", 42 | accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 43 | }; 44 | 45 | const { data } = await axios.get(url, { headers }); 46 | const extractData = data.replace(/"/g, '"').replace(/&/g, "&"); 47 | 48 | const videoUrl = this.match(extractData, /"browser_native_hd_url":"(.*?)"/, /hd_src\s*:\s*"([^"]*)"/, 49 | /"browser_native_sd_url":"(.*?)"/, /sd_src\s*:\s*"([^"]*)"/)?.[1]; 50 | const title = this.match(extractData, /(.*?)<\/title>/)?.[1] || "Facebook Video"; 52 | const thumbnail = this.match(extractData, /"preferred_thumbnail":{"image":{"uri":"(.*?)"/)?.[1]; 53 | 54 | if (!videoUrl) { 55 | throw new Error("can't find download link"); 56 | } 57 | 58 | return { 59 | platform: 'facebook', 60 | metadata: { 61 | title: this.parseString(title), 62 | thumbnail: this.parseString(thumbnail || ''), 63 | author: { 64 | name: 'Facebook User' 65 | } 66 | }, 67 | downloads: [{ 68 | type: 'video', 69 | url: this.parseString(videoUrl), 70 | quality: 'Original', 71 | filename: `facebook_${Date.now()}.mp4` 72 | }] 73 | }; 74 | 75 | } catch (error) { 76 | console.error("Facebook Scraping Error:", error); 77 | throw new Error(error.message || "error while getting data from facebook"); 78 | } 79 | } 80 | 81 | match(data, ...patterns) { 82 | for (const pattern of patterns) { 83 | const result = data.match(pattern); 84 | if (result) return result; 85 | } 86 | return null; 87 | } 88 | 89 | parseString(string) { 90 | try { 91 | return JSON.parse(`{"text": "${string}"}`).text; 92 | } catch (e) { 93 | return string; 94 | } 95 | } 96 | } 97 | 98 | module.exports = FacebookDownloader; 99 | -------------------------------------------------------------------------------- /src/scrape/instagram.js: -------------------------------------------------------------------------------- 1 | const FIXED_TIMESTAMP = 1739185749634; 2 | const SECRECT_KEY = "46e9243172efe7ed14fa58a98949d9e3a6cc7ec3aa0ae5d21c1654e507de884c"; 3 | const BASE_URL = "https://instasupersave.com"; 4 | const URL_MSEC = "/msec"; 5 | const URL_CONVERT = "/api/convert"; 6 | 7 | class InstagramDownloader { 8 | constructor(url) { 9 | this.url = url; 10 | this.headers = { 11 | "authority": "instasupersave.com", 12 | "accept": "*/*", 13 | "accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7", 14 | "content-type": "application/json", 15 | "origin": "https://instasupersave.com", 16 | "referer": "https://instasupersave.com/en/", 17 | "sec-fetch-dest": "empty", 18 | "sec-fetch-mode": "cors", 19 | "sec-fetch-site": "same-origin", 20 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" 21 | }; 22 | } 23 | 24 | async req(url, method = "GET", data = null) { 25 | try { 26 | const response = await fetch(url, { 27 | method, 28 | headers: this.headers, 29 | ...(data ? { body: data } : {}) 30 | }); 31 | 32 | if (!response.ok) { 33 | throw new Error(`HTTP error! status: ${response.status}`); 34 | } 35 | 36 | return response; 37 | } catch (error) { 38 | console.error('Request Error:', error); 39 | throw error; 40 | } 41 | } 42 | 43 | sort(obj) { 44 | return Object.keys(obj).sort().reduce((result, key) => { 45 | result[key] = obj[key]; 46 | return result; 47 | }, {}); 48 | } 49 | 50 | async genSignature(input) { 51 | try { 52 | const rs = await this.req(BASE_URL + URL_MSEC); 53 | const { msec } = await rs.json(); 54 | 55 | let serverTime = Math.floor(msec * 1000); 56 | let timeDiff = serverTime ? Date.now() - serverTime : 0; 57 | 58 | if (Math.abs(timeDiff) < 60000) { 59 | timeDiff = 0; 60 | } 61 | 62 | const timestamp = Date.now() - timeDiff; 63 | const payload = typeof input === "string" ? input : JSON.stringify(this.sort(input)); 64 | const digest = `${payload}${timestamp}${SECRECT_KEY}`; 65 | 66 | const encoder = new TextEncoder(); 67 | const data = encoder.encode(digest); 68 | const hashBuffer = await crypto.subtle.digest("SHA-256", data); 69 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 70 | const signature = hashArray.map(byte => byte.toString(16).padStart(2, "0")).join(""); 71 | 72 | return { 73 | url: input, 74 | ts: timestamp, 75 | _ts: FIXED_TIMESTAMP, 76 | _tsc: timeDiff, 77 | _s: signature 78 | }; 79 | } catch (error) { 80 | console.error('Signature Generation Error:', error); 81 | throw error; 82 | } 83 | } 84 | 85 | cleanUrl(url) { 86 | try { 87 | const urlObj = new URL(url); 88 | urlObj.search = ''; return urlObj.toString(); 89 | } catch (error) { 90 | return url; 91 | } 92 | } 93 | 94 | async download() { 95 | try { 96 | const signature = await this.genSignature(this.url); 97 | console.log('Sending request with signature:', signature); 98 | 99 | const response = await this.req( 100 | BASE_URL + URL_CONVERT, 101 | "POST", 102 | JSON.stringify(signature) 103 | ); 104 | 105 | const result = await response.json(); 106 | console.log('API Response:', result); 107 | const postDate = new Date(result.meta?.taken_at * 1000); 108 | const formattedDate = postDate.toLocaleString('id-ID', { 109 | day: 'numeric', 110 | month: 'long', 111 | year: 'numeric', 112 | hour: '2-digit', 113 | minute: '2-digit' 114 | }); 115 | 116 | return { 117 | platform: 'instagram', 118 | caption: result.meta?.title || '', 119 | author: result.meta?.username || '', 120 | username: result.meta?.username || '', 121 | 'img-thumb': result.thumb || null, 122 | like: result.meta?.like_count || 0, 123 | views: 0, 124 | comments: result.meta?.comment_count || 0, 125 | date: formattedDate, 126 | downloads: result.url.map(media => { 127 | let type = 'download_video_hd'; 128 | if (media.type === 'jpg' || media.type === 'jpeg' || media.type === 'png') { 129 | type = 'download_image'; 130 | } 131 | 132 | return { 133 | type: type, 134 | url: media.url, 135 | filename: `instagram_${Date.now()}.${media.ext}` 136 | }; 137 | }) 138 | }; 139 | 140 | } catch (error) { 141 | console.error('Instagram Download Error:', error); 142 | throw new Error('Gagal mengunduh dari Instagram: ' + error.message); 143 | } 144 | } 145 | } 146 | 147 | module.exports = InstagramDownloader; -------------------------------------------------------------------------------- /src/scrape/snackvideo.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | 4 | class SnackVideoDownloader { 5 | constructor(url) { 6 | this.url = url; 7 | } 8 | 9 | async download() { 10 | try { 11 | const response = await axios.get(this.url); 12 | const $ = cheerio.load(response.data); 13 | 14 | const videoData = $("#VideoObject").text().trim(); 15 | if (!videoData) { 16 | throw new Error("Tidak dapat menemukan data video"); 17 | } 18 | 19 | const videoInfo = JSON.parse(videoData); 20 | if (!videoInfo.contentUrl) { 21 | throw new Error("Tidak dapat menemukan URL video"); 22 | } 23 | 24 | 25 | const uploadDate = videoInfo.uploadDate ? new Date(videoInfo.uploadDate) : new Date(); 26 | const formattedDate = uploadDate.toLocaleString('id-ID', { 27 | day: 'numeric', 28 | month: 'long', 29 | year: 'numeric', 30 | hour: '2-digit', 31 | minute: '2-digit' 32 | }); 33 | 34 | return { 35 | platform: 'snackvideo', 36 | caption: videoInfo.name || videoInfo.description || '', 37 | author: videoInfo.author?.name || '', 38 | username: videoInfo.author?.name || '', 39 | 'img-thumb': videoInfo.thumbnailUrl || null, 40 | like: parseInt(videoInfo.interactionStatistic?.userInteractionCount) || 0, 41 | views: parseInt(videoInfo.interactionCount) || 0, 42 | comments: 0, 43 | date: formattedDate, 44 | downloads: [{ 45 | type: 'download_video_hd', 46 | url: videoInfo.contentUrl, 47 | filename: `snackvideo_${Date.now()}.mp4` 48 | }] 49 | }; 50 | 51 | } catch (error) { 52 | console.error("SnackVideo Download Error:", error); 53 | throw new Error("Gagal mengunduh dari SnackVideo: " + error.message); 54 | } 55 | } 56 | } 57 | 58 | module.exports = SnackVideoDownloader; 59 | -------------------------------------------------------------------------------- /src/scrape/soundcloud.js: -------------------------------------------------------------------------------- 1 | const scdl = require('soundcloud-downloader').default; 2 | 3 | class SoundCloudDownloader { 4 | constructor(url) { 5 | this.url = url; 6 | this.CLIENT_ID = 'yLfooVZK5emWPvRLZQlSuGTO8pof6z4t'; 7 | } 8 | 9 | formatDuration(ms) { 10 | const minutes = Math.floor(ms / 60000); 11 | const seconds = ((ms % 60000) / 1000).toFixed(0); 12 | return `${minutes}:${seconds.padStart(2, '0')}`; 13 | } 14 | 15 | async download() { 16 | try { 17 | if (!this.url.includes('soundcloud.com')) { 18 | throw new Error('URL SoundCloud tidak valid'); 19 | } 20 | 21 | const info = await scdl.getInfo(this.url, this.CLIENT_ID); 22 | 23 | 24 | const uploadDate = new Date(info.created_at); 25 | const formattedDate = uploadDate.toLocaleString('id-ID', { 26 | day: 'numeric', 27 | month: 'long', 28 | year: 'numeric', 29 | hour: '2-digit', 30 | minute: '2-digit' 31 | }); 32 | 33 | return { 34 | platform: 'soundcloud', 35 | caption: info.title, 36 | author: info.user.username, 37 | username: info.user.permalink, 38 | 'img-thumb': info.artwork_url?.replace('-large', '-t500x500') || info.user.avatar_url, 39 | like: info.likes_count || 0, 40 | views: info.playback_count || 0, 41 | comments: info.comment_count || 0, 42 | date: formattedDate, 43 | downloads: [{ 44 | type: 'download_audio', 45 | url: this.url, 46 | filename: `${info.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${info.id}.mp3` 47 | }] 48 | }; 49 | 50 | } catch (error) { 51 | console.error('SoundCloud Download Error:', error); 52 | throw new Error('Gagal mengunduh dari SoundCloud: ' + error.message); 53 | } 54 | } 55 | 56 | async downloadAudio() { 57 | try { 58 | const stream = await scdl.download(this.url, this.CLIENT_ID); 59 | 60 | const chunks = []; 61 | const audioBuffer = await new Promise((resolve, reject) => { 62 | stream.on('data', chunk => chunks.push(chunk)); 63 | stream.on('end', () => resolve(Buffer.concat(chunks))); 64 | stream.on('error', reject); 65 | }); 66 | 67 | return audioBuffer; 68 | } catch (error) { 69 | console.error('Audio Download Error:', error); 70 | throw new Error('Gagal mengunduh audio: ' + error.message); 71 | } 72 | } 73 | } 74 | 75 | module.exports = SoundCloudDownloader; 76 | -------------------------------------------------------------------------------- /src/scrape/spotify.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const platformPatterns = require('../system/patterns'); 3 | 4 | class SpotifyDownloader { 5 | constructor(url) { 6 | this.url = url; 7 | } 8 | 9 | async download() { 10 | const BASEURL = "https://api.fabdl.com"; 11 | const headers = { 12 | Accept: "application/json, text/plain, */*", 13 | "Content-Type": "application/json", 14 | "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36", 15 | }; 16 | 17 | try { 18 | if (!platformPatterns.spotify.test(this.url)) { 19 | throw new Error('URL Spotify tidak valid'); 20 | } 21 | 22 | const response = await axios.get(`${BASEURL}/spotify/get?url=${this.url}`, { headers }); 23 | const info = response.data; 24 | 25 | console.log('API Response:', info); 26 | 27 | if (!info.result) { 28 | throw new Error("Tidak ada hasil ditemukan dalam respons."); 29 | } 30 | 31 | const { gid, id, name, image, duration_ms } = info.result; 32 | 33 | const downloadResponse = await axios.get(`${BASEURL}/spotify/mp3-convert-task/${gid}/${id}`, { headers }); 34 | const downloadInfo = downloadResponse.data; 35 | 36 | console.log('Download API Response:', downloadInfo); 37 | 38 | if (!downloadInfo.result || !downloadInfo.result.download_url) { 39 | throw new Error("Download URL tidak ditemukan dalam respons."); 40 | } 41 | 42 | return { 43 | platform: 'spotify', 44 | downloads: [{ 45 | type: 'audio', 46 | url: `${BASEURL}${downloadInfo.result.download_url}`, 47 | filename: `${name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.mp3` 48 | }], 49 | metadata: { 50 | title: name, 51 | duration: duration_ms, 52 | cover: image, 53 | } 54 | }; 55 | } catch (error) { 56 | console.error("Error downloading Spotify track:", error.message); 57 | throw new Error(error.message); 58 | } 59 | } 60 | } 61 | 62 | module.exports = SpotifyDownloader; -------------------------------------------------------------------------------- /src/scrape/terabox.js: -------------------------------------------------------------------------------- 1 | class TeraboxDownloader { 2 | constructor(url) { 3 | this.url = url; 4 | } 5 | 6 | async getInfo() { 7 | try { 8 | const url = `https://terabox.hnn.workers.dev/api/get-info?shorturl=${this.url.split("/").pop()}&pwd=`; 9 | const headers = { 10 | "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Mobile Safari/537.36", 11 | "Referer": "https://terabox.hnn.workers.dev/", 12 | }; 13 | const response = await fetch(url, { headers }); 14 | if (!response.ok) { 15 | throw new Error(`Gagal mengambil informasi file: ${response.status} ${response.statusText}`); 16 | } 17 | return await response.json(); 18 | } catch (error) { 19 | console.error("Gagal mengambil informasi file:", error); 20 | throw error; 21 | } 22 | } 23 | 24 | async getDownloadLink(fsId, shareid, uk, sign, timestamp) { 25 | try { 26 | const url = "https://terabox.hnn.workers.dev/api/get-download"; 27 | const headers = { 28 | "Content-Type": "application/json", 29 | "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Mobile Safari/537.36", 30 | "Referer": "https://terabox.hnn.workers.dev/", 31 | }; 32 | const data = { 33 | shareid: shareid, 34 | uk: uk, 35 | sign: sign, 36 | timestamp: timestamp, 37 | fs_id: fsId, 38 | }; 39 | const response = await fetch(url, { 40 | method: "POST", 41 | headers: headers, 42 | body: JSON.stringify(data), 43 | }); 44 | if (!response.ok) { 45 | throw new Error(`Gagal mengambil link download: ${response.status} ${response.statusText}`); 46 | } 47 | return await response.json(); 48 | } catch (error) { 49 | console.error("Gagal mengambil link download:", error); 50 | throw error; 51 | } 52 | } 53 | 54 | async download() { 55 | try { 56 | const { list, shareid, uk, sign, timestamp } = await this.getInfo(); 57 | if (!list) { 58 | throw new Error("File tidak ditemukan"); 59 | } 60 | 61 | const downloads = await Promise.all(list.map(async (file) => { 62 | const { downloadLink } = await this.getDownloadLink(file.fs_id, shareid, uk, sign, timestamp); 63 | 64 | 65 | let type = 'download_video_hd'; 66 | const ext = file.filename.split('.').pop().toLowerCase(); 67 | 68 | if (['mp4', 'mkv', 'avi'].includes(ext)) { 69 | type = 'download_video_hd'; 70 | } else if (['mp3', 'wav', 'ogg'].includes(ext)) { 71 | type = 'download_audio'; 72 | } else if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) { 73 | type = 'download_image'; 74 | } 75 | 76 | return { 77 | type: type, 78 | url: downloadLink, 79 | filename: file.filename, 80 | size: file.size 81 | }; 82 | })); 83 | 84 | 85 | return { 86 | platform: 'terabox', 87 | caption: list[0].filename, 88 | author: 'Terabox User', 89 | username: 'terabox_user', 90 | 'img-thumb': list[0].thumbs?.url || '', 91 | like: 0, 92 | views: 0, 93 | comments: 0, 94 | date: new Date().toLocaleDateString('id-ID', { 95 | day: 'numeric', 96 | month: 'long', 97 | year: 'numeric', 98 | hour: '2-digit', 99 | minute: '2-digit' 100 | }), 101 | downloads: downloads 102 | }; 103 | } catch (error) { 104 | console.error("Terabox Download Error:", error); 105 | throw new Error("Gagal mengunduh dari Terabox: " + error.message); 106 | } 107 | } 108 | } 109 | 110 | module.exports = TeraboxDownloader; -------------------------------------------------------------------------------- /src/scrape/threads.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | class ThreadsDownloader { 4 | constructor(url) { 5 | this.url = url; 6 | this.FAKE_AGENTS = [ 7 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 8 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 9 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 10 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0' 11 | ]; 12 | } 13 | 14 | async download() { 15 | try { 16 | if (!this.url.match(/threads\.net/gi)) { 17 | throw new Error('URL Threads tidak valid'); 18 | } 19 | 20 | const apiResponse = await axios.get(`https://api.threadsphotodownloader.com/v2/media`, { 21 | params: { url: this.url }, 22 | headers: { 23 | 'User-Agent': this.FAKE_AGENTS[Math.floor(Math.random() * this.FAKE_AGENTS.length)], 24 | 'Accept': '*/*', 25 | 'Origin': 'https://sssthreads.pro', 26 | 'Referer': 'https://sssthreads.pro/' 27 | }, 28 | timeout: 30000 29 | }); 30 | 31 | if (!apiResponse.data) { 32 | throw new Error('Tidak ada data dari API'); 33 | } 34 | 35 | const downloads = []; 36 | 37 | 38 | if (apiResponse.data.video_urls && apiResponse.data.video_urls.length > 0) { 39 | apiResponse.data.video_urls.forEach((video, index) => { 40 | if (video.download_url) { 41 | 42 | let type = 'download_video_hd'; 43 | if (video.quality) { 44 | switch(video.quality.toLowerCase()) { 45 | case '1080p': 46 | type = 'download_video_1080p'; 47 | break; 48 | case '720p': 49 | type = 'download_video_720p'; 50 | break; 51 | case '480p': 52 | type = 'download_video_480p'; 53 | break; 54 | case '360p': 55 | type = 'download_video_360p'; 56 | break; 57 | case '240p': 58 | type = 'download_video_240p'; 59 | break; 60 | default: 61 | type = 'download_video_hd'; 62 | } 63 | } 64 | 65 | downloads.push({ 66 | type: type, 67 | url: video.download_url, 68 | filename: `threads_video_${Date.now()}_${index + 1}.mp4` 69 | }); 70 | } 71 | }); 72 | } 73 | 74 | 75 | if (apiResponse.data.image_urls && apiResponse.data.image_urls.length > 0) { 76 | apiResponse.data.image_urls.forEach((imageUrl, index) => { 77 | downloads.push({ 78 | type: 'download_image', 79 | url: imageUrl, 80 | filename: `threads_image_${Date.now()}_${index + 1}.jpg` 81 | }); 82 | }); 83 | } 84 | 85 | if (downloads.length === 0) { 86 | throw new Error('Tidak dapat menemukan media'); 87 | } 88 | 89 | 90 | return { 91 | platform: 'threads', 92 | caption: apiResponse.data.text || '', 93 | author: apiResponse.data.author?.full_name || 'Threads User', 94 | username: apiResponse.data.author?.username || 'threads_user', 95 | 'img-thumb': apiResponse.data.thumbnail_url || apiResponse.data.image_urls?.[0] || '', 96 | like: apiResponse.data.likes_count || 0, 97 | views: apiResponse.data.view_count || 0, 98 | comments: apiResponse.data.comments_count || 0, 99 | date: new Date(apiResponse.data.created_at || Date.now()).toLocaleDateString('id-ID', { 100 | day: 'numeric', 101 | month: 'long', 102 | year: 'numeric', 103 | hour: '2-digit', 104 | minute: '2-digit' 105 | }), 106 | downloads: downloads 107 | }; 108 | 109 | } catch (error) { 110 | console.error('Threads Download Error:', error); 111 | if (error.response) { 112 | console.error('Error Response:', error.response.data); 113 | } 114 | throw new Error('Gagal mengunduh dari Threads: ' + error.message); 115 | } 116 | } 117 | 118 | async downloadMedia(url) { 119 | try { 120 | const response = await axios.get(url, { 121 | responseType: 'arraybuffer', 122 | headers: { 123 | 'User-Agent': this.FAKE_AGENTS[Math.floor(Math.random() * this.FAKE_AGENTS.length)] 124 | } 125 | }); 126 | 127 | return { 128 | data: response.data, 129 | contentType: response.headers['content-type'] 130 | }; 131 | } catch (error) { 132 | throw new Error('Gagal mengunduh media: ' + error.message); 133 | } 134 | } 135 | } 136 | 137 | module.exports = ThreadsDownloader; -------------------------------------------------------------------------------- /src/scrape/tiktok.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | class TikTokDownloader { 4 | constructor(url) { 5 | this.url = url; 6 | this.scrapers = { 7 | api1: this.api1.bind(this), 8 | api2: this.api2.bind(this), 9 | api3: this.api3.bind(this), 10 | api4: this.api4.bind(this), 11 | api5: this.api5.bind(this) 12 | }; 13 | } 14 | 15 | async download() { 16 | for (const scraper of Object.values(this.scrapers)) { 17 | try { 18 | const result = await scraper(); 19 | if (result) { 20 | return result; 21 | } 22 | } catch (error) { 23 | console.error(`Scraper error (${scraper.name}):`, error.message); 24 | continue; 25 | } 26 | } 27 | throw new Error('Semua metode scraping gagal'); 28 | } 29 | 30 | async api1() { 31 | try { 32 | const response = await axios.post('https://www.tikwm.com/api/', { 33 | url: this.url, 34 | count: 12, 35 | cursor: 0, 36 | web: 1, 37 | hd: 1 38 | }); 39 | 40 | if (response.data.code === 0) { 41 | const data = response.data.data; 42 | 43 | const getOriginalUrl = async (url) => { 44 | try { 45 | const headResponse = await axios.head(url, { 46 | maxRedirects: 0, 47 | validateStatus: (status) => status >= 200 && status < 400 48 | }); 49 | return headResponse.headers.location || url; 50 | } catch (error) { 51 | return url; 52 | } 53 | }; 54 | 55 | const urls = await Promise.all([ 56 | getOriginalUrl('https://www.tikwm.com' + (data.hdplay || data.play)), 57 | getOriginalUrl('https://www.tikwm.com' + data.wmplay), 58 | getOriginalUrl('https://www.tikwm.com' + data.music) 59 | ]); 60 | 61 | return { 62 | platform: 'tiktok', 63 | caption: data.title, 64 | author: data.author.nickname, 65 | username: data.author.unique_id, 66 | 'img-thumb': data.cover, 67 | like: parseInt(data.digg_count) || 0, 68 | views: parseInt(data.play_count) || 0, 69 | comments: parseInt(data.comment_count) || 0, 70 | date: new Date(data.create_time * 1000).toLocaleDateString('id-ID', { 71 | day: 'numeric', 72 | month: 'long', 73 | year: 'numeric', 74 | hour: '2-digit', 75 | minute: '2-digit' 76 | }), 77 | downloads: [ 78 | { 79 | type: 'download_video_hd', 80 | url: urls[0], 81 | filename: `tiktok_${data.author.unique_id}_hd.mp4` 82 | }, 83 | { 84 | type: 'download_video_480p', 85 | url: urls[1], 86 | filename: `tiktok_${data.author.unique_id}_watermark.mp4` 87 | }, 88 | { 89 | type: 'download_audio', 90 | url: urls[2], 91 | filename: `tiktok_${data.author.unique_id}_audio.mp3` 92 | } 93 | ] 94 | }; 95 | } 96 | return null; 97 | } catch (error) { 98 | throw new Error('API 1 Error: ' + error.message); 99 | } 100 | } 101 | 102 | async api2() { 103 | try { 104 | const indexResponse = await axios.get('https://ttdownloader.com/'); 105 | const token = indexResponse.data.match(/value="([0-9a-z]+)"/)[1]; 106 | 107 | const formData = new URLSearchParams(); 108 | formData.append('url', this.url); 109 | formData.append('format', ''); 110 | formData.append('token', token); 111 | 112 | const response = await axios.post('https://ttdownloader.com/search/', formData); 113 | const urls = response.data.match(/(https?:\/\/.*?\.php\?v=.*?)\"/g); 114 | 115 | if (!urls) return null; 116 | 117 | const username = this.url.split('@')[1]?.split('/')[0] || 'unknown'; 118 | 119 | return { 120 | platform: 'tiktok', 121 | caption: 'TikTok Video', 122 | author: 'TikTok User', 123 | username: username, 124 | 'img-thumb': '', 125 | like: 0, 126 | views: 0, 127 | comments: 0, 128 | date: new Date().toLocaleDateString('id-ID', { 129 | day: 'numeric', 130 | month: 'long', 131 | year: 'numeric', 132 | hour: '2-digit', 133 | minute: '2-digit' 134 | }), 135 | downloads: [ 136 | { 137 | type: 'download_video_hd', 138 | url: urls[0].replace(/\"$/, ''), 139 | filename: `tiktok_${username}_hd.mp4` 140 | } 141 | ] 142 | }; 143 | } catch (error) { 144 | throw new Error('API 2 Error: ' + error.message); 145 | } 146 | } 147 | 148 | async api3() { 149 | throw new Error('API 3 belum diimplementasi'); 150 | } 151 | 152 | async api4() { 153 | throw new Error('API 4 belum diimplementasi'); 154 | } 155 | 156 | async api5() { 157 | throw new Error('API 5 belum diimplementasi'); 158 | } 159 | } 160 | 161 | module.exports = TikTokDownloader; -------------------------------------------------------------------------------- /src/scrape/xiaohongshu.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const cheerio = require('cheerio'); 3 | const patterns = require('../system/patterns'); 4 | 5 | class XiaohongshuDownloader { 6 | constructor(url) { 7 | this.url = url; 8 | this.FAKE_AGENTS = [ 9 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 10 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 11 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 12 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0' 13 | ]; 14 | } 15 | 16 | removeUnicode(jsonString) { 17 | try { 18 | const cleanJson = jsonString 19 | .replace(/[\u0000-\u001F\u007F-\u009F]/g, "") 20 | .replace(/\\u/g, '') 21 | .replace(/\\n/g, ' ') 22 | .replace(/002F/g, "/") 23 | .replace(/undefined/g, "null") 24 | .replace(/\\r/g, ' ') 25 | .replace(/\\t/g, ' ') 26 | .replace(/\\f/g, ' ') 27 | .replace(/\\b/g, ' ') 28 | .replace(/\\\\/g, '\\') 29 | .replace(/\\'/g, "'") 30 | .replace(/\\"/g, '"') 31 | .replace(/\s+/g, ' ') 32 | .trim(); 33 | 34 | return cleanJson; 35 | } catch (error) { 36 | console.error('Error cleaning JSON:', error); 37 | throw new Error('Gagal membersihkan JSON string'); 38 | } 39 | } 40 | 41 | fixImageUrl(url) { 42 | if (url.startsWith('http://')) { 43 | url = 'https://' + url.slice(7); 44 | } 45 | return url; 46 | } 47 | 48 | async download() { 49 | try { 50 | if (!patterns.xiaohongshu.test(this.url)) { 51 | throw new Error('URL Xiaohongshu tidak valid'); 52 | } 53 | 54 | const headers = { 55 | 'User-Agent': this.FAKE_AGENTS[Math.floor(Math.random() * this.FAKE_AGENTS.length)], 56 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 57 | 'Accept-Language': 'en-US,en;q=0.5', 58 | 'Referer': 'https://www.xiaohongshu.com/', 59 | 'Cookie': 'webId=auto;' 60 | }; 61 | 62 | const response = await axios.get(this.url, { headers }); 63 | const html = response.data; 64 | const $ = cheerio.load(html); 65 | 66 | let scriptContent = ''; 67 | $('script').each((i, elem) => { 68 | const content = $(elem).html(); 69 | if (content && content.includes('window.__INITIAL_STATE__=')) { 70 | scriptContent = content; 71 | } 72 | }); 73 | 74 | if (!scriptContent) { 75 | throw new Error('Tidak dapat menemukan data konten'); 76 | } 77 | 78 | const jsonString = scriptContent.split('window.__INITIAL_STATE__=')[1].split(';')[0]; 79 | const cleanJsonString = this.removeUnicode(jsonString); 80 | 81 | const data = JSON.parse(cleanJsonString); 82 | 83 | if (!data.note || !data.note.currentNoteId) { 84 | throw new Error('Format data tidak valid'); 85 | } 86 | 87 | const id = data.note.currentNoteId; 88 | const meta = data.note.noteDetailMap[id].note; 89 | const downloads = []; 90 | 91 | if (meta.video && meta.video.media && meta.video.media.stream && meta.video.media.stream.h264) { 92 | downloads.push({ 93 | type: 'download_video_hd', 94 | url: this.fixImageUrl(meta.video.media.stream.h264[0].masterUrl), 95 | filename: `xiaohongshu_${meta.user.nickname}_video.mp4` 96 | }); 97 | } else if (meta.imageList && Array.isArray(meta.imageList)) { 98 | meta.imageList.forEach((img, index) => { 99 | if (img.urlDefault) { 100 | downloads.push({ 101 | type: 'download_image', 102 | url: this.fixImageUrl(img.urlDefault), 103 | filename: `xiaohongshu_${meta.user.nickname}_image_${index + 1}.jpg` 104 | }); 105 | } 106 | }); 107 | } 108 | 109 | if (downloads.length === 0) { 110 | throw new Error('Tidak ada media yang dapat diunduh'); 111 | } 112 | 113 | return { 114 | platform: 'xiaohongshu', 115 | caption: meta.title || meta.desc || '', 116 | author: meta.user.nickname || 'Xiaohongshu User', 117 | username: meta.user.nickname || 'xiaohongshu_user', 118 | 'img-thumb': this.fixImageUrl(meta.cover?.urlDefault || meta.imageList?.[0]?.urlDefault || ''), 119 | like: parseInt(meta.likeCount) || 0, 120 | views: parseInt(meta.viewCount) || 0, 121 | comments: parseInt(meta.commentCount) || 0, 122 | date: new Date(meta.time || Date.now()).toLocaleDateString('id-ID', { 123 | day: 'numeric', 124 | month: 'long', 125 | year: 'numeric', 126 | hour: '2-digit', 127 | minute: '2-digit' 128 | }), 129 | downloads: downloads 130 | }; 131 | 132 | } catch (error) { 133 | console.error('Xiaohongshu Download Error:', error); 134 | if (error.response) { 135 | console.error('Response Error:', error.response.data); 136 | } 137 | throw new Error('Gagal mengunduh dari Xiaohongshu: ' + error.message); 138 | } 139 | } 140 | 141 | async downloadMedia(url) { 142 | try { 143 | const response = await axios.get(url, { 144 | responseType: 'arraybuffer', 145 | headers: { 146 | 'User-Agent': this.FAKE_AGENTS[Math.floor(Math.random() * this.FAKE_AGENTS.length)] 147 | } 148 | }); 149 | 150 | return { 151 | data: response.data, 152 | contentType: response.headers['content-type'] 153 | }; 154 | } catch (error) { 155 | throw new Error('Gagal mengunduh media: ' + error.message); 156 | } 157 | } 158 | } 159 | 160 | module.exports = XiaohongshuDownloader; -------------------------------------------------------------------------------- /src/system/patterns.js: -------------------------------------------------------------------------------- 1 | const platformPatterns = { 2 | tiktok: /(?:https?:\/\/)?(?:www\.|vm\.|vt\.|m\.)?(?:tiktok\.com|tiktokcdn\.com)(?:\/.*)?/i, 3 | capcut: /(?:https?:\/\/)?(?:www\.|m\.)?(?:capcut\.com|capcutpro\.com)(?:\/.*)?/i, 4 | xiaohongshu: /(?:https?:\/\/)?(?:www\.|m\.)?(?:xiaohongshu\.com|xhslink\.com|xhs\.cn)(?:\/.*)?/i, 5 | threads: /(?:https?:\/\/)?(?:www\.|m\.)?threads\.net(?:\/.*)?/i, 6 | soundcloud: /(?:https?:\/\/)?(?:www\.|m\.)?(?:soundcloud\.com|snd\.sc)(?:\/.*)?/i, 7 | spotify: /(?:https?:\/\/)?(?:open\.)?spotify\.com(?:\/.*)?/i, 8 | facebook: /(?:https?:\/\/)?(?:www\.|m\.)?facebook\.com(?:\/.*)?/i, 9 | instagram: /(?:https?:\/\/)?(?:www\.|m\.)?instagram\.com(?:\/.*)?/i, 10 | terabox: /(?:https?:\/\/)?(?:www\.|m\.)?(?:terabox\.com|teraboxapp\.com)(?:\/.*)?/i, 11 | snackvideo: /(?:https?:\/\/)?(?:www\.|m\.)?snackvideo\.com(?:\/.*)?/i 12 | }; 13 | 14 | module.exports = platformPatterns; 15 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "server.js", 6 | "use": "@vercel/node" 7 | }, 8 | { 9 | "src": "public/**", 10 | "use": "@vercel/static" 11 | } 12 | ], 13 | "routes": [ 14 | { 15 | "src": "/js/(.*)", 16 | "dest": "/public/js/$1" 17 | }, 18 | { 19 | "src": "/css/(.*)", 20 | "dest": "/public/css/$1" 21 | }, 22 | { 23 | "src": "/static/(.*)", 24 | "dest": "/public/$1" 25 | }, 26 | { 27 | "src": "/(.*)", 28 | "dest": "server.js" 29 | } 30 | ], 31 | "env": { 32 | "NODE_ENV": "production" 33 | } 34 | } 35 | --------------------------------------------------------------------------------