76 |
77 |
196 |
197 |
198 |
200 |
201 |
202 |
203 |
204 |
205 |
209 |
210 |
211 |
219 |
220 |
222 |
223 | Download
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
Download Result
233 |
234 |
236 |
237 |
238 |
239 |
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 |
284 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
300 |
301 |
302 |
303 |
304 |
305 |
336 |
337 |
338 |
340 |
341 |
342 |
343 | Frequently Asked Questions
344 |
345 |
346 |
359 |
360 |
361 |
364 |
365 |
367 |
368 |
369 |
370 |
371 |
373 |
374 |
385 |
386 |
387 |
388 |
389 |
390 |
404 |
405 |
406 |
407 |
408 |
409 |
418 |
419 |
447 |
530 |
605 |
606 |
614 |
615 |
653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
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 |
80 |
81 |
82 |
83 |
84 |
85 |
87 |
88 |
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 |
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 |
276 |
277 |
278 |
279 |
280 | ${downloadData.downloads.map(download => `
281 |
283 |
284 |
285 |
286 |
287 |
288 |
289 | ${getDisplayName(download.type)}
290 |
291 |
292 | Click to download
293 |
294 |
295 |
296 |
297 |
298 |
299 |
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 |
460 |
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 |
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 |
660 | ` : `
661 |
662 |
663 |
664 | `}
665 |
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 |
688 |
689 | Download Media
690 |
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 |
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 |
--------------------------------------------------------------------------------