Download photos and videos from Threads quickly and easily!
// ==UserScript== // @name Threads Video Downloader // @namespace https://github.com/ManoloZocco/Threads-video-downloader-userscript // @version 1.3.11 // @description Download photos and videos from Threads quickly and easily! // @author P0L1T3 aka Manolo Zocco // @match https://*.threads.net/* // @connect * // @grant GM_download // @grant GM_addStyle // @grant GM_xmlhttpRequest // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // Inserisce il CSS (prende spunto da interface.css dell'addon Firefox) GM_addStyle(` .dw { position: absolute; z-index: 5; width: 116px; height: 34px; border-radius: 8px; background: rgba(0, 0, 0, 0.6); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); display: none; flex-direction: row; justify-content: space-around; align-items: center; cursor: pointer; margin: 7px; border: none; color: #FFF; font-size: 11px; font-weight: 600; text-transform: uppercase; line-height: 22px; letter-spacing: -0.42px; bottom: 7px; left: 7px; } .dw .icon { background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 3v9' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M6 10l4 4 4-4' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Crect x='4' y='14' width='12' height='2' fill='white'/%3E%3C/svg%3E"); width: 20px; height: 20px; margin-right: 5px; } *:hover > .dw { display: flex; } .dw:hover { background: rgba(0, 0, 0, 0.8); } `); // Funzione per il download: utilizza GM_download (con fallback se necessario) function downloadFile(url) { if (!url) return; let fileName = url.substring(url.lastIndexOf('/') + 1) || 'download'; GM_download({ url: url, name: fileName, onerror: function(err) { console.error('GM_download error:', err); fallbackDownload(url, fileName); } }); } function fallbackDownload(url, fileName) { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", onload: function(response) { const blob = response.response; const blobUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = blobUrl; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, onerror: function(error) { console.error('Fallback download error:', error); } }); } // Oggetto per iniettare il pulsante come fa l'addon Firefox (injector.js) const downloader = { observeDom() { // Per i video: usa semplicemente l'attributo "src" come fa l'addon Firefox document.querySelectorAll("video").forEach((video) => { let container = this.findRoot(video); if (!container) return; if (container.querySelector(".dw")) return; let url = video.getAttribute("src") || null; // Aggiungi il pulsante solo se l'URL contiene ".mp4" if (url && url.toLowerCase().indexOf(".mp4") !== -1) { container.appendChild(this.getBtn(url)); } }); // Per le immagini: usa "src" e, se l'immagine è grande, la inserisce document.querySelectorAll("img").forEach((img) => { if (img.width < 200 || img.height < 200) return; if (img.parentElement.querySelector(".dw")) return; let url = img.getAttribute("src") || null; if (url && (url.toLowerCase().endsWith(".jpg") || url.toLowerCase().endsWith(".jpeg") || url.toLowerCase().endsWith(".png") || url.toLowerCase().endsWith(".gif"))) { img.parentElement.prepend(this.getBtn(url)); } }); }, // Crea il pulsante di download; imita getBtn dell'addon Firefox usando browser.i18n.getMessage("btn_title") getBtn(url) { let btn = document.createElement("button"); btn.innerText = "Download"; btn.className = "dw"; let icon = document.createElement("span"); icon.className = "icon"; btn.appendChild(icon); btn.setAttribute("src", url); btn.addEventListener("click", this.dw); return btn; }, // Cerca ricorsivamente un container adatto (come fa findRoot nell'addon Firefox) findRoot(el) { let parent = el.parentNode; if (!parent) return null; let candidate = parent.querySelector("div[data-visualcompletion]"); return candidate || this.findRoot(parent); }, // Handler del click: esegue il download inviando il "src" come fa l'addon Firefox dw(event) { event.preventDefault(); event.stopPropagation(); let btn = (event.target.nodeName.toLowerCase() === "button") ? event.target : event.target.parentElement; let url = btn.hasAttribute("src") ? btn.getAttribute("src") : null; if (url) { downloadFile(url); } } }; // Inietta il pulsante a intervalli (simile al setInterval in injector.js) function init() { downloader.observeDom(); } setInterval(init, 500); new MutationObserver(init).observe(document.body, { childList: true, subtree: true }); })();