Tiktok Video & Slideshow Downloader 🎬🖼️

Download TikTok videos without watermark and slideshow images

// ==UserScript==
// @name         Tiktok Video & Slideshow Downloader 🎬🖼️
// @namespace    https://greasyfork.org/en/scripts/431826
// @version      2.6
// @description  Download TikTok videos without watermark and slideshow images
// @author       YAD
// @match        *://*.tiktok.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @icon         https://miro.medium.com/v2/resize:fit:512/1*KX6NTUUHWlCP4sCXz28TBA.png
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const getFileName = (url, type) => {
        const id = url.split('/').pop().split('?')[0];
        return type === 'video' ? `TikTok_Video_${id}.mp4` : `TikTok_Image_${id}.jpeg`;
    };

    const createButton = (icon, color, clickHandler) => {
        const button = document.createElement('div');
        Object.assign(button.style, {
            position: 'absolute',
            right: '15px',
            top: '27%',
            transform: 'translateY(-50%)',
            width: '50px',
            height: '50px',
            backgroundColor: color,
            color: '#ffffff',
            fontSize: '18px',
            textShadow: '3px 3px 0px #9C1331',
            textAlign: 'center',
            lineHeight: '50px',
            borderRadius: '50%',
            cursor: 'pointer',
            zIndex: '999999'
        });
        button.textContent = icon;
        button.onclick = clickHandler;
        return button;
    };

    const downloadContent = (url, filename, type, button, isZip = false) => {
        if (!url) {
            button.textContent = '✖️';
            return;
        }

        button.textContent = '⏳';

        GM_xmlhttpRequest({
            method: 'GET',
            url,
            responseType: 'blob',
            onload: ({ response }) => {
                if (response) {
                    if (isZip) {
                        filename.file(type, response);
                    } else {
                        GM_download({
                            url: URL.createObjectURL(response),
                            name: filename,
                            onload: () => {
                                button.textContent = '✔️';
                                setTimeout(() => button.remove(), 2000);
                            },
                            onerror: () => {
                                button.textContent = '✖️';
                                setTimeout(() => button.remove(), 1500);
                            }
                        });
                    }
                } else {
                    button.textContent = '✖️';
                    setTimeout(() => button.remove(), 1500);
                }
            },
            onerror: () => {
                button.textContent = '✖️';
                setTimeout(() => button.remove(), 1500);
            },
            ontimeout: () => {
                button.textContent = '✖️';
                setTimeout(() => button.remove(), 1500);
            }
        });
    };

    const createDownloadButton = (video) => {
        const button = createButton('🎞️', '#ff3b5c', async (e) => {
            e.stopPropagation();
            e.preventDefault();
            button.textContent = '⏳';

            const videoUrl = video.src || video.querySelector('source')?.src;

            if (videoUrl && videoUrl.startsWith('blob:')) {
                button.style.backgroundColor = '#ffa700';

                const xgwrapper = document.querySelector('[id^="xgwrapper-"]');
                const videoId = xgwrapper?.id.split('-')[2];
                const tiktokVideoUrl = `https://www.tiktok.com/@YAD/video/${videoId}`;

                const iframe = document.createElement('iframe');
                iframe.style.position = 'fixed';
                iframe.style.visibility = 'hidden';
                iframe.src = tiktokVideoUrl;
                document.body.appendChild(iframe);

                const checkVideoUrl = () => {
                    const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
                    const videoElement = iframeDocument.querySelector('video');
                    if (videoElement && !videoElement.src.startsWith('blob:')) {
                        downloadContent(videoElement.src, getFileName(videoElement.src, 'video'), 'video', button);
                        iframe.remove();
                    } else {
                        setTimeout(checkVideoUrl, 1000);
                    }
                };

                setTimeout(checkVideoUrl, 8000);
            } else {
                button.style.backgroundColor = '#ff3b5c';
                downloadContent(videoUrl, getFileName(videoUrl, 'video'), 'video', button);
            }
        });

        video.parentNode.style.position = 'relative';
        video.parentNode.appendChild(button);
        return button;
    };

    const manageDownloadButtons = (video) => {
        let button;
        video.addEventListener('mouseover', () => {
            if (!button) {
                button = createDownloadButton(video);
            }
        });
        video.addEventListener('mouseout', (e) => {
            if (button && !video.contains(e.relatedTarget) && !button.contains(e.relatedTarget)) {
                // Only remove the button if it's not in the loading state (⏳)
                if (button.textContent !== '⏳') {
                    button.remove();
                    button = null;
                }
            }
        });
    };


    const promptUserForZipOrIndividual = () => {
        return new Promise((resolve) => {
            const userChoice = confirm("Do you want to download images as a ZIP file? Cancel will download individually");
            resolve(userChoice);
        });
    };

    const addImageDownloadButton = (container) => {
        if (container.querySelector('.image-download-btn')) return;

        const button = createButton('🖼️', '#16b1c6', async () => {
            button.textContent = '⌛';
            const images = container.querySelectorAll('img');

            if (!images.length) {
                alert("No images found!");
                button.textContent = '✖️';
                return;
            }

            const imageUrls = Array.from(images).map(img => img.src);
            const uniqueUrls = [...new Set(imageUrls)];

            const downloadAsZip = await promptUserForZipOrIndividual();

            if (downloadAsZip) {
                const zip = new JSZip();
                let count = 0;
                uniqueUrls.forEach((url, index) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url,
                        responseType: 'blob',
                        onload: (response) => {
                            zip.file(`image${index + 1}.jpeg`, response.response);
                            count++;
                            if (count === uniqueUrls.length) {
                                zip.generateAsync({ type: 'blob' }).then((content) => {
                                    const url = URL.createObjectURL(content);
                                    GM_download({ url, name: 'TikTok_Slideshow.zip' });
                                    button.textContent = '✔️';
                                });
                            }
                        }
                    });
                });
            } else {
                uniqueUrls.forEach((url, index) => {
                    GM_download({
                        url,
                        name: `image${index + 1}.jpeg`,
                        onload: () => {
                            button.textContent = '✔️';
                        }
                    });
                });
            }
        });

        container.style.position = 'relative';
        container.appendChild(button);
    };

    const checkForImageSlideshows = () => {
        document.querySelectorAll('div[class*="DivPhotoVideoContainer"]:not(.processed)').forEach((container) => {
            container.classList.add('processed');
            addImageDownloadButton(container);
        });
    };

    new MutationObserver(checkForImageSlideshows).observe(document.body, { childList: true, subtree: true });

    new MutationObserver(() => {
        document.querySelectorAll('video:not(.processed)').forEach((video) => {
            video.classList.add('processed');
            manageDownloadButtons(video);
        });
    }).observe(document.body, { childList: true, subtree: true });

})();