TwitterImg Downloader

Add download button to Twitter image, and click to download the original image named by format.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         TwitterImg Downloader
// @namespace    TypeNANA
// @version      0.15
// @description  Add download button to Twitter image, and click to download the original image named by format.
// @author       HY
// @include      *://twitter.com/*
// @include      *://*.twitter.com/*
// @include      *://x.com/*
// @include      *://*.x.com/*
// @require      http://code.jquery.com/jquery-3.3.1.min.js
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    /** Edit defaultFileName to change the file name format
     *
     *  <%Userid>  Twitter user ID.        eg: shiratamacaron
     *  <%Tid>     Tweet ID.              eg: 1095705491429158912
     *  <%Time>    Current timestamp.     eg: 1550557810891
     *  <%PicName> Original pic name.     eg: DzS6RkJUUAA_0LX
     *  <%PicNo>   Ordinal number of pic. eg: 0
     *
     *  default: "<%Userid> <%Tid>_p<%PicNo>"
     *    result: "shiratamacaron 1095705491429158912_p0.jpg"
     *
     *  example1: "<%Userid> <%Tid> <%PicName>”
     *    result: "shiratamacaron 1095705491429158912 DzS6RkJUUAA_0LX.jpg"
     *
     *  example2: "<%Tid>_p<%PicNo>”
     *    result: "1095705491429158912_p0.jpg"
     */
    let defaultFileName = "<%Userid> <%Tid>_p<%PicNo>";


    /** Edit following value to change download shortcut key in gallery mode
     *  KeyCode value can be found at https://keycode.info/
     *
     *  default: shift + s (s->83)
     */
    let shortCut_Shift = true; //true - Yes , false - No
    let shortCut_Ctrl = false;
    let shortCut_Alt = false;
    let shortCut_KeyCode = 83 //KeyCode value

    function download(url, name, view) {
        //通过fetch获取blob
        fetch(url).then(response => {
            if (response.status == 200)
                return response.blob();
            throw new Error(`status: ${response.status}.`)
        }).then(blob => {
            downloadFile(name, blob, view)
        }).catch(error => {
            console.log("failed. cause:", error)
        })
    }

    function downloadFile(fileName, blob, view) {
        //通过a标签的download属性来下载指定文件名的文件
        let anchor = view;
        let src = URL.createObjectURL(blob);
        anchor.download = fileName;
        anchor.href = src;
        view.click();
    }

    const addDownloadButton = function (v) {
        if (newVersionFlag) {
            newVer(v);
        } else {
            oldVer(v)
        }
        return false;
    }

    function addDlBtn(){
        let btnDownloadImg;
        if(document.getElementsByClassName("img-link").length==0){
            btnDownloadImg = document.createElement('A');
            btnDownloadImg.className = 'img-link';
            document.getElementById("react-root").appendChild(btnDownloadImg);
        }else{
            btnDownloadImg = document.getElementsByClassName("img-link")[0];
        }
        return btnDownloadImg;
    }

    function newVer(v) {
        if (v == null || v.length == 0) return;
        let target = v[0];
        if (target == null || target.src == null) return;
        if (target.alt == null || target.alt == "") return;
        if (target.parentElement.getAttribute("aria-label") == null || target.parentElement.getAttribute("aria-label") == "") return;
        let dlbtn = document.createElement('DIV');
        target.parentElement.parentElement.appendChild(dlbtn);
        dlbtn.outerHTML = '<div class="dl_btn_div" style="cursor: pointer;z-index: 999;display: table;font-size: 15px;color: white;position: absolute;right: 5px;bottom: 5px;background: #0000007f;height: 30px;width: 30px;border-radius: 15px;text-align: center;"><svg class="icon" style="width: 15px;height: 15px;vertical-align: top;display: inline-block;margin-top: 7px;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3658"><path d="M925.248 356.928l-258.176-258.176a64 64 0 0 0-45.248-18.752H144a64 64 0 0 0-64 64v736a64 64 0 0 0 64 64h736a64 64 0 0 0 64-64V402.176a64 64 0 0 0-18.752-45.248zM288 144h192V256H288V144z m448 736H288V736h448v144z m144 0H800V704a32 32 0 0 0-32-32H256a32 32 0 0 0-32 32v176H144v-736H224V288a32 32 0 0 0 32 32h256a32 32 0 0 0 32-32V144h77.824l258.176 258.176V880z" p-id="3659"></path></svg></div>';
        dlbtn = target.parentElement.parentElement.getElementsByClassName("dl_btn_div")[0];

        let btnDownloadImg = addDlBtn();
        let urlregex = /https\:\/\/(twitter|x).com\//;

        if (!document.location.href.includes("photo")) {
            //信息流模式

            let firstA = findFirstA(target);
            //获取文件名
            // https://pbs.twimg.com/media/D_mR-WEUYAAZJVH?format=jpg&amp;name=360x360
            let fooName = target.src.split("?")[0];
            let barName = fooName.split("/");
            let dl_picname = barName[barName.length - 1];
            let dl_time = new Date().getTime();

            //获取图片编号
            // ameto_y/status/1151067160078274561/photo/1
            let array = firstA.href.replace(urlregex, "").split("/");
            let dl_userid = array[0];
            let dl_tid = array[2];
            let dl_picno = array[4];
            //替换内容,拼接文件名
            let dl_filename = defaultFileName
                .replace("<%Userid>", dl_userid)
                .replace("<%Tid>", dl_tid)
                .replace("<%Time>", dl_time)
                .replace("<%PicName>", dl_picname)
                .replace("<%PicNo>", dl_picno - 1);
            //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
            let dl_ext = "jpg";
            if (target.src.includes("format=png")) {
                dl_ext = "png";
            }
            dlbtn.addEventListener('touchstart', function (e) {
                dlbtn.onclick = function (e) {
                    return false;
                }
                return false;
            });
            dlbtn.addEventListener('mousedown', function (e) {
                dlbtn.onclick = function (e) {
                    return false;
                }
                return false;
            });
            dlbtn.addEventListener('click', function (e) {
                //调用下载方法
                cancelBubble(e);
                download("https://pbs.twimg.com/media/" + dl_picname + "?format=" + dl_ext + "&name=orig", dl_filename + "." + dl_ext, btnDownloadImg);
                return false;
            });
        } else {
            //大图画廊模式

            //获取文件名
            // https://pbs.twimg.com/media/D_mR-WEUYAAZJVH?format=jpg&amp;name=360x360
            let fooName = target.src.split("?")[0];
            let barName = fooName.split("/");
            let dl_picname = barName[barName.length - 1];

            //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
            let dl_ext = "jpg";
            if (target.src.includes("format=png")) {
                dl_ext = "png";
            }
            dlbtn.addEventListener('click', function (e) {
                //调用下载方法
                cancelBubble(e);

                //获取图片编号
                // ameto_y/status/1151067160078274561/photo/1
                let array = document.location.href.replace(urlregex, "").split("/");
                let dl_userid = array[0];
                let dl_tid = array[2];
                let dl_picno = array[4];
                let dl_time = new Date().getTime();
                let dl_filename = defaultFileName
                    .replace("<%Userid>", dl_userid)
                    .replace("<%Tid>", dl_tid)
                    .replace("<%Time>", dl_time)
                    .replace("<%PicName>", dl_picname)
                    .replace("<%PicNo>", dl_picno - 1);
                download("https://pbs.twimg.com/media/" + dl_picname + "?format=" + dl_ext + "&name=orig", dl_filename + "." + dl_ext, btnDownloadImg);
                return false;
            });
        }
    }

    //画廊模式下的快捷键功能
    function onShortCut() {
        let locationArr = document.location.href.split("/");
        let targetArr = $('ul[role="list"]');

        let imgNo = null;
        let imgArr = null;
        let target = null;
        //判断是否找到了画廊的ul标签
        if (targetArr.length == 0 ) {
            //如果找不到ul标签,并且不是画廊模式(那么网址内没有“photo”), 则不进行进一步的处理
            if(locationArr.length < 2 || locationArr[locationArr.length - 2] != "photo") return;
            //否则进行搜索单张图画廊的情况
            var arr = $('img[src^="https://pbs.twimg.com/media/');
            for(var i=0;i< arr.length;i++){
                var imgUrl = arr[i].src.split("?")[i]
                //判断是否是推特附带的图片img,并且判断是不是信息流的图片(信息流图片的所有母层中必定带有一个a标签)
                if(arr[i].parentElement.firstElementChild!="" && arr[i].parentElement.firstElementChild.style.backgroundImage.includes(imgUrl) && findFirstA(arr[i])==null){
                    //单图模式的赋值
                    let imgNo = 0;
                    target = arr[i];
                    break;
                }
            }
            //如果找不到任何目标,则不进行进一步的处理
            if(target == null) return;
        } else {
            //多图模式的赋值
            imgNo = locationArr[locationArr.length - 1] - 1;
            imgArr = targetArr[0].getElementsByTagName("img");
            target = imgArr[imgNo];
        }

        //获取文件名
        // https://pbs.twimg.com/media/D_mR-WEUYAAZJVH?format=jpg&amp;name=360x360
        let fooName = target.src.split("?")[0];
        let barName = fooName.split("/");
        let dl_picname = barName[barName.length - 1];

        //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
        let dl_ext = "jpg";
        if (target.src.includes("format=png")) {
            dl_ext = "png";
        }
        //获取图片编号
        // ameto_y/status/1151067160078274561/photo/1
        let urlregex = /https\:\/\/(twitter|x).com\//;
        let array = document.location.href.replace(urlregex, "").split("/");
        let dl_userid = array[0];
        let dl_tid = array[2];
        let dl_picno = array[4];
        let dl_time = new Date().getTime();
        let dl_filename = defaultFileName
            .replace("<%Userid>", dl_userid)
            .replace("<%Tid>", dl_tid)
            .replace("<%Time>", dl_time)
            .replace("<%PicName>", dl_picname)
            .replace("<%PicNo>", dl_picno - 1);

        let btnDownloadImg = addDlBtn();
        download("https://pbs.twimg.com/media/" + dl_picname + "?format=" + dl_ext + "&name=orig", dl_filename + "." + dl_ext, btnDownloadImg);
    }

    function oldVer(v) {
        let tweets = document.querySelectorAll('.tweet');
        tweets.forEach((t) => {
            //忽略视频信息
            if (t.getElementsByClassName("PlayableMedia").length > 0) return;
            //文件名信息
            let dl_userid = t.getAttribute("data-screen-name");
            let dl_name = t.getAttribute("data-name");
            let dl_tid = t.getAttribute("data-tweet-id");
            //尝试获取发推时间,但是部分情况无法获取,故采用保存文件时间
            //let dl_time = t.getElementsByClassName("_timestamp")[0].getAttribute("data-time");
            let dl_time = new Date().getTime();
            /* 画廊 */
            if (t.parentElement.className.includes("GalleryTweet")) {
                //获取画廊容器
                let imgContent = t.parentElement.parentElement.getElementsByClassName("Gallery-media")[0];
                //防止按钮重复叠加
                if (imgContent.parentElement.parentElement.getElementsByClassName("dl_btn_div").length != 0) return;
                //创建下载按钮
                let dlbtn = document.createElement('div');
                imgContent.parentElement.appendChild(dlbtn);
                dlbtn.outerHTML = '<div class="dl_btn_div" style="z-index: 999;display: table;font-size: 15px;color: white;position: absolute;right: 5px;top: 5px;background: #0000007f;height: 30px;width: 30px;border-radius: 15px;text-align: center;"><a style="display: table-cell;height: 30px;width: 30px;vertical-align: middle;color:white;font-family:edgeicons;text-decoration: none;user-select: none;" id="a_dl">&#xf088</a></div>';
                dlbtn = imgContent.parentElement.getElementsByClassName("dl_btn_div")[0];
                //创建不可见的下载用标签
                let btnDownloadImg = document.createElement('A');
                btnDownloadImg.className = 'img-link';
                imgContent.parentElement.parentElement.appendChild(btnDownloadImg);
                //添加点击事件
                dlbtn.addEventListener('click', function () {
                    //去掉图片链接尾部的 ":large"
                    let ImgUrl = imgContent.getElementsByClassName("media-image")[0].src.replace(":large", "");
                    //获取文件名
                    let dl_picname = ImgUrl.replace('https://pbs.twimg.com/media/', '').replace('.png', '').replace('.jpg', '');
                    //设置默认图片编号0
                    let dl_picno = 0;
                    //个人页面class
                    let Images = imgContent.parentElement.querySelectorAll('.AdaptiveMedia-container img');
                    if (Images.length <= 0) {
                        //信息流class
                        Images = imgContent.parentElement.querySelectorAll('.AdaptiveMedia-photoContainer img');
                    }
                    //通过循环比较获取图片序号
                    for (var imgNo = 0; imgNo < Images.length; imgNo++) {
                        if (ImgUrl == Images[imgNo].src) {
                            dl_picno = imgNo;
                            break;
                        }
                    }
                    //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
                    let dl_ext = ".jpg";
                    if (ImgUrl.includes(".png")) {
                        dl_ext = ".png";
                    }
                    //替换内容,拼接文件名
                    let dl_filename = defaultFileName
                        .replace("<%Userid>", dl_userid)
                        .replace("<%Name>", dl_name)
                        .replace("<%Tid>", dl_tid)
                        .replace("<%Time>", dl_time)
                        .replace("<%PicName>", dl_picname)
                        .replace("<%PicNo>", dl_picno);
                    //调用下载方法
                    download(ImgUrl + ":orig", dl_filename + dl_ext, btnDownloadImg);
                });
                return;
            }
            /* 信息流 */
            //防止按钮重复叠加
            if (t.getElementsByClassName("dl_btn_div").length != 0) return;
            //获取全部图片标签
            let Images = t.querySelectorAll('.AdaptiveMedia-container img');
            for (var i = 0; i < Images.length; i++) {
                let Img = Images[i];
                if (Img) {
                    //获取图片链接
                    let ImgUrl = Img.src;
                    //如果为blob对象则跳过
                    if (Img.src.includes('blob')) break;
                    //创建下载按钮
                    let dlbtn = document.createElement('div');
                    Img.parentElement.parentElement.appendChild(dlbtn);
                    dlbtn.outerHTML = '<div class="dl_btn_div" style="display: table;font-size: 15px;color: white;position: absolute;right: 5px;bottom: 5px;background: #0000007f;height: 30px;width: 30px;border-radius: 15px;text-align: center;"><a style="display: table-cell;height: 30px;width: 30px;vertical-align: middle;color:white;font-family:edgeicons;text-decoration: none;user-select: none;" id="a_dl">&#xf088</a></div>';
                    dlbtn = Img.parentElement.parentElement.getElementsByClassName("dl_btn_div")[0];
                    //创建不可见的下载用标签
                    let btnDownloadImg = document.createElement('A');
                    btnDownloadImg.className = 'img-link';
                    t.appendChild(btnDownloadImg);
                    //获取文件名
                    let dl_picname = Img.src.replace('https://pbs.twimg.com/media/', '').replace('.png', '').replace('.jpg', '');
                    //获取图片编号
                    let dl_picno = i;
                    //替换内容,拼接文件名
                    let dl_filename = defaultFileName
                        .replace("<%Userid>", dl_userid)
                        .replace("<%Name>", dl_name)
                        .replace("<%Tid>", dl_tid)
                        .replace("<%Time>", dl_time)
                        .replace("<%PicName>", dl_picname)
                        .replace("<%PicNo>", dl_picno);
                    //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
                    let dl_ext = ".jpg";
                    if (ImgUrl.includes(".png")) {
                        dl_ext = ".png";
                    }
                    //添加点击事件
                    dlbtn.addEventListener('click', function () {
                        //调用下载方法
                        download(ImgUrl + ":orig", dl_filename + dl_ext, btnDownloadImg);
                    });
                }
            };
        });
    }

    let newVersionFlag = (document.getElementById("react-root") != null);
    if (newVersionFlag) {
        waitForKeyElements(
            'img[src^="https://pbs.twimg.com/media/"]',
            addDownloadButton
        );
    } else {
        waitForKeyElements(
            '.AdaptiveMedia-container img',
            addDownloadButton
        );
    }
    $(document).keyup(function (e) {
        let shiftFlag = true
        let ctrlFlag = true
        let altFlag = true
        if (shortCut_Shift) {
            shiftFlag = e.shiftKey
        }
        if (shortCut_Ctrl) {
            ctrlFlag = e.ctrlKey
        }
        if (shortCut_Alt) {
            altFlag = e.altKey
        }
        if (e.keyCode == shortCut_KeyCode && shiftFlag && ctrlFlag && altFlag) {
            onShortCut()
        }
    })

    function waitForKeyElements(
        selectorTxt,
        actionFunction,
        bWaitOnce,
        iframeSelector
    ) {
        var targetNodes, btargetsFound;

        if (typeof iframeSelector == "undefined") {
            targetNodes = $(selectorTxt);
        } else {
            targetNodes = $(iframeSelector).contents().find(selectorTxt);
        }

        if (targetNodes && targetNodes.length > 0) {
            btargetsFound = true;
            targetNodes.each(function () {
                var jThis = $(this);
                var alreadyFound = jThis.data('alreadyFound') || false;

                if (!alreadyFound) {
                    var cancelFound = actionFunction(jThis);
                    if (cancelFound) {
                        btargetsFound = false;
                    } else {
                        jThis.data('alreadyFound', true);
                    }
                }
            });
        } else {
            btargetsFound = false;
        }

        var controlObj = waitForKeyElements.controlObj || {};
        var controlKey = selectorTxt.replace(/[^\w]/g, "_");
        var timeControl = controlObj[controlKey];

        if (btargetsFound && bWaitOnce && timeControl) {
            clearInterval(timeControl);
            delete controlObj[controlKey]
        } else {
            if (!timeControl) {
                timeControl = setInterval(function () {
                    waitForKeyElements(selectorTxt,
                        actionFunction,
                        bWaitOnce,
                        iframeSelector
                    );
                }, 300);
                controlObj[controlKey] = timeControl;
            }
        }
        waitForKeyElements.controlObj = controlObj;
    }

    function findFirstA(node) {
        var tmp = node;
        for (var i = 0; i < 20; i++) {
            tmp = tmp.parentElement
            if (tmp == null) return null;
            if (tmp.nodeName == "a" || tmp.nodeName == "A") {
                return tmp
            }
        }
    }
    function findFirstLi(node) {
        var tmp = node;
        for (var i = 0; i < 20; i++) {
            tmp = tmp.parentElement
            if (tmp == null) return null;
            if (tmp.nodeName == "li" || tmp.nodeName == "LI") {
                return tmp
            }
        }
    }
    function cancelBubble(e) {
        var evt = e ? e : window.event;
        if (evt.stopPropagation) {
            evt.stopPropagation();
        } else {
            evt.cancelBubble = true;
        }
    }
})();