Twitter 下載影片

即按前往下載影片的網頁。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name                Twitter: Download Video
// @name:zh-TW          Twitter 下載影片
// @name:zh-CN          Twitter 下载视频
// @name:ja             Twitter ビデオをダウンロード
// @name:ko             Twitter 비디오 다운로드
// @name:ru             Twitter Скачать видео
// @version             1.0.7
// @description         One button click to direct video download web page.
// @description:zh-TW   即按前往下載影片的網頁。
// @description:zh-CN   一键导向下载视频的网页。
// @description:ja      ボタンをクリックして、ビデオのダウンロードWebページに移動します。
// @description:ko      한 번의 클릭으로 비디오 다운로드 웹 사이트를 탐색하십시오.
// @description:ru      Нажмите кнопку, чтобы перейти на страницу загрузки видео.
// @author              Hayao-Gai
// @namespace           https://github.com/HayaoGai
// @icon                https://i.imgur.com/M9oO8K9.png
// @include             https://twitter.com/*
// @include             https://mobile.twitter.com/*
// @grant               none
// ==/UserScript==

/* jshint esversion: 6 */

(function() {
    'use strict';

    // icons made by https://www.flaticon.com/authors/freepik
    const svg = `<svg viewBox="0 0 512 512"><path d="M472,313v139c0,11.028-8.972,20-20,20H60c-11.028,0-20-8.972-20-20V313H0v139c0,33.084,26.916,60,60,60h392 c33.084,0,60-26.916,60-60V313H472z"></path></g></g><g><g><polygon points="352,235.716 276,311.716 276,0 236,0 236,311.716 160,235.716 131.716,264 256,388.284 380.284,264"></polygon></svg>`;
    const resource = "https://www.savetweetvid.com/result?url=";
    let currentUrl = document.location.href;
    let updating = false;

    init(10);

    locationChange();

    window.addEventListener("scroll", update);

    function init(times) {
        for (let i = 0; i < times; i++) {
            setTimeout(findVideo1, 500 * i);
            setTimeout(findVideo2, 500 * i);
            setTimeout(sensitiveContent, 500 * i);
        }
    }

    function findVideo1() {
        // video play button
        document.querySelectorAll("[data-testid='playButton']").forEach(button => {
            // thumbnail
            button.parentElement.querySelectorAll("img:not(.download-set)").forEach(thumbnail => {
                thumbnail.classList.add("download-set");
                const url = thumbnail.src;
                situation(url, thumbnail);
            });
        });
    }

    function findVideo2() {
        // video
        document.querySelectorAll("video:not(.download-set)").forEach(video => {
            video.classList.add("download-set");
            const url = video.poster;
            situation(url, video);
        });
    }

    function situation(url, video) {
        // situation 1: gif
        if (url.includes("tweet_")) findMenu(video, "gif");
        // situation 2: video
        else if (url.includes("ext_tw_") || url.includes("amplify_") || url.includes("media")) findMenu(video, "video");
        // situation 3: unknown
        else console.log("Error: Unknown");
    }

    function findMenu(child, type) {
        const article = child.closest("article:not(.article-set)");
        if (!article) return;
        article.classList.add("article-set");
        const menus = article.querySelectorAll("[data-testid='caret']");
        menus.forEach(menu => menu.addEventListener("click", () => {
            clickMenu(article, type, false);
            if (type === "gif") clickMenu(article, type, true);
        }));
    }

    function clickMenu(article, type, convert) {
        // check exist.
        if (!!document.querySelector(`.option-download-${convert}-set`)) return;
        // wait menu.
        if (!document.querySelector("[role='menuitem']")) {
            setTimeout(() => clickMenu(article, type, convert), 100);
            return;
        }
        const menu = document.querySelector("[role='menuitem']").parentElement;
        // add "download" option.
        const option = document.createElement("div");
        option.className = "css-1dbjc4n r-1loqt21 r-18u37iz r-1ny4l3l r-ymttw5 r-1yzf0co r-o7ynqc r-6416eg r-13qz1uu option-download-set";
        option.addEventListener("mouseenter", () => option.classList.add(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"])));
        option.addEventListener("mouseleave", () => option.classList.remove(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"])));
        option.addEventListener("click", () => clickDownload(article, type, convert));
        // icon
        const icon = document.createElement("div");
        icon.className = "css-1dbjc4n r-1777fci";
        icon.innerHTML = svg;
        const svgElement = icon.querySelector("svg");
        svgElement.setAttribute("class", "r-4qtqp9 r-yyyyoo r-1q142lx r-1xvli5t r-zso239 r-dnmrzs r-bnwqim r-1plcrui r-lrvibr");
        svgElement.classList.add(getTheme(["r-1re7ezh", "r-9ilb82", "r-111h2gw"]));
        // text
        const text1 = document.createElement("div");
        text1.className = "css-1dbjc4n r-16y2uox r-1wbh5a2";
        const text2 = document.createElement("div");
        text2.className = "css-901oao r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0";
        text2.classList.add(getTheme(["r-hkyrab", "r-1fmj7o5", "r-jwli3a"]));
        const text3 = document.createElement("span");
        text3.className = "css-901oao css-16my406 r-1qd0xha r-ad9z0x r-bcqeeo r-qvutc0";
        text3.innerText = getLocalization(type, convert);
        // append
        menu.appendChild(option);
        option.appendChild(icon);
        option.appendChild(text1);
        text1.appendChild(text2);
        text2.appendChild(text3);
    }

    function clickDownload(article, type, convert) {
        // gif
        if (type === "gif" && !convert) {
            let link;
            // condition 1: not play yet.
            article.querySelectorAll("video").forEach(video => {
                link = video.src;
            });
            // condition 2: playing.
            if (!link) {
                const image = [...article.querySelectorAll("img")].find(image => image.src.includes("video"));
                const id = image.src.split(/[/?]/)[4];
                link = `https://video.twimg.com/tweet_video/${id}.mp4`;
            }
            // open
            window.open(link);
        }
        // video
        else {
            const title = article.querySelector("time");
            const url = !!title ? title.parentElement.href : window.location.href;
            window.open(`${resource}${url}`);
        }
    }

    function getTheme(array) {
        const body = document.querySelector("body");
        const color = body.style.backgroundColor; // "rgb(21, 32, 43)"
        const red = color.match(/\d+/)[0]; // "21"
        switch (red) {
            case "255":
                return array[0]; // white
            case "0":
                return array[1]; // black
            default:
                return array[2]; // gray
        }
    }

    function getLocalization(type, convert) {
        let download = "Download";
        switch (document.querySelector("html").lang) {
            case "zh-Hant":
                download = "下載";
                break;
            case "zh":
                download = "下载";
                break;
            case "ja":
                download = "ダウンロード";
                break;
            case "ko":
                download = "다운로드";
                break;
            case "ru":
                download = "Скачать";
                break;
        }

        let extension = "";
        if (type === "gif") extension = convert ? " GIF" : " MP4";

        return `${download}${extension}`;
    }

    function sensitiveContent() {
        // click "view" button on sensitive content warning to run this script again.
        document.querySelectorAll(".r-42olwf.r-1vsu8ta:not(.view-set)").forEach(view => {
            view.classList.add("view-set");
            view.addEventListener("click", () => init(3));
        });
    }

    function update() {
        if (updating) return;
        updating = true;
        init(3);
        setTimeout(() => { updating = false; }, 1000);
    }

    function locationChange() {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(() => {
                if (currentUrl !== document.location.href) {
                    currentUrl = document.location.href;
                    init(10);
                }
            });
        });
        const target = document.body;
        const config = { childList: true, subtree: true };
        observer.observe(target, config);
    }

})();