// ==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 });
})();