Threads Video Downloader

Download photos and videos from Threads quickly and easily!

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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