Extract and download videos from Threads
// ==UserScript==
// @name ThreadsDownloader
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Extract and download videos from Threads
// @author Just Enough
// @homepageURL https://www.conech.net/
// @match *://*.threads.com/*
// @grant none
// @license MIT2
// ==/UserScript==
(function () {
'use strict';
const BUTTON_CLASS = 'threads-extract-download-btn';
const generateFilename = () => {
const now = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const timestamp = [
now.getFullYear(),
pad(now.getMonth() + 1),
pad(now.getDate())
].join('') + '_' + [
pad(now.getHours()),
pad(now.getMinutes()),
pad(now.getSeconds())
].join('');
return `ThreadsExtract_${timestamp}.mp4`;
};
const createDownloadIcon = () => {
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', '18');
svg.setAttribute('height', '18');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'white');
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', 'M12 16l-5-5h3V4h4v7h3l-5 5zm-7 2h14v2H5v-2z');
svg.appendChild(path);
return svg;
};
const findRootContainer = (element) => {
let current = element;
while (current && current.parentElement) {
const parent = current.parentElement;
if (parent.querySelector('video')) {
return parent;
}
current = parent;
}
return null;
};
const downloadVideo = async (video) => {
const src = video.currentSrc || video.src;
if (!src) {
alert('Video source not found');
return;
}
try {
const response = await fetch(src);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = generateFilename();
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(blobUrl);
document.body.removeChild(link);
} catch (error) {
console.error('Download failed:', error);
alert('Failed to download video');
}
};
const createDownloadButton = (video) => {
const button = document.createElement('button');
button.className = BUTTON_CLASS;
button.appendChild(createDownloadIcon());
Object.assign(button.style, {
position: 'absolute',
top: '8px',
right: '8px',
zIndex: '999999',
width: '32px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,0.6)',
border: 'none',
borderRadius: '50%',
cursor: 'pointer'
});
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
downloadVideo(video);
}, true);
return button;
};
const attachButtonToPlayer = (player, video) => {
if (player.querySelector(`.${BUTTON_CLASS}`)) return;
const computedStyle = window.getComputedStyle(player);
if (computedStyle.position === 'static') {
player.style.position = 'relative';
}
const button = createDownloadButton(video);
player.appendChild(button);
};
const scanPlayers = () => {
const players = document.querySelectorAll('[aria-label="Video player"]');
players.forEach((player) => {
const root = findRootContainer(player);
if (!root) return;
const video = root.querySelector('video');
if (!video) return;
attachButtonToPlayer(player, video);
});
};
const initObserver = () => {
const observer = new MutationObserver(scanPlayers);
observer.observe(document.body, {
childList: true,
subtree: true
});
};
const init = () => {
scanPlayers();
initObserver();
};
init();
})();