Threads Video Downloader

Download photos and videos from Threads quickly and easily!

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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