Greasy Fork

Greasy Fork is available in English.

Tiktok Video & Slideshow Downloader 🎬🖼️

Download TikTok videos without watermark and slideshow images

目前为 2024-09-13 提交的版本。查看 最新版本

// ==UserScript==
// @name         Tiktok Video & Slideshow Downloader 🎬🖼️
// @namespace    http://greasyfork.icu/en/scripts/431826
// @version      2.3
// @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 downloadFile = (url, type, button) => {
        if (!url) {
            button.textContent = '✖️';
            return;
        }

        button.textContent = '⏳';
        GM_xmlhttpRequest({
            method: 'GET',
            url,
            responseType: 'blob',
            onload: ({ response }) => {
                if (response) {
                    GM_download({
                        url: URL.createObjectURL(response),
                        name: getFileName(url, type),
                        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 = document.createElement('div');
        Object.assign(button.style, {
            position: 'absolute',
            right: '15px',
            top: '27%',
            transform: 'translateY(-50%)',
            width: '50px',
            height: '50px',
            backgroundColor: '#ff3b5c',
            color: '#ffffff',
            fontSize: '18px',
            textShadow: '3px 3px 0px #9C1331',
            textAlign: 'center',
            lineHeight: '50px',
            borderRadius: '50%',
            cursor: 'pointer',
            zIndex: '99999'
        });
        button.textContent = '🎞️';

        button.onclick = 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:')) {
                        downloadFile(videoElement.src, 'video', button);
                        iframe.remove();
                    } else {
                        setTimeout(checkVideoUrl, 1000);
                    }
                };

                setTimeout(checkVideoUrl, 8000);
            } else {
                button.style.backgroundColor = '#ff3b5c';
                downloadFile(videoUrl, '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)) {
                button.remove();
                button = null;
            }
        });
    };

    const checkForNewVideos = () => {
        document.querySelectorAll('video:not(.processed)').forEach((video) => {
            video.classList.add('processed');
            manageDownloadButtons(video);
        });
        requestAnimationFrame(checkForNewVideos);
    };

    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 = document.createElement('div');
        button.className = 'image-download-btn';
        button.textContent = '🖼️';
        Object.assign(button.style, {
            position: 'absolute',
            right: '15px',
            top: '27%',
            transform: 'translateY(-50%)',
            width: '50px',
            height: '50px',
            backgroundColor: '#16b1c6',
            color: '#ffffff',
            fontSize: '18px',
            textShadow: '3px 3px 0px #9C1331',
            textAlign: 'center',
            lineHeight: '50px',
            borderRadius: '50%',
            cursor: 'pointer',
            zIndex: '999999'
        });

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

        button.onclick = 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 = '✔️';
                        }
                    });
                });
            }
        };
    };

    const checkForImageSlideshows = () => {
        document.querySelectorAll('.css-kgj69c-DivPhotoVideoContainer:not(.processed)').forEach((container) => {
            container.classList.add('processed');
            addImageDownloadButton(container);
        });
        requestAnimationFrame(checkForImageSlideshows);
    };

    requestAnimationFrame(checkForNewVideos);
    requestAnimationFrame(checkForImageSlideshows);
})();