Skip to content

Instantly share code, notes, and snippets.

@tizee
Last active July 15, 2023 15:13
Show Gist options
  • Save tizee/e5ad9746ff8cd17c4303672ea704952d to your computer and use it in GitHub Desktop.
Save tizee/e5ad9746ff8cd17c4303672ea704952d to your computer and use it in GitHub Desktop.
Twitter video download script for Tampermonkey
// ==UserScript==
// @name twitter video downloader
// @namespace http://tampermonkey.net/
// @version 1.0
// @description download twitter video
// @author tizee
// @match https://twitter.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function () {
"use strict";
function getVideoResolutionQuality(videoUrl) {
const videoRegex =
/^https:\/\/video.twimg.com\/(ext_tw_video|amplify_video)\/\d+\/(pu\/)?vid\/(?<resolution>\w+)\/.*$/;
console.debug(videoUrl);
const res = videoUrl.match(videoRegex).groups.resolution;
// there are only 2 number strings
return res.split("x").reduce((acc, cur) => {
return Number.parseInt(acc) * Number.parseInt(cur);
});
}
async function getTweetMedia(tweetId = "") {
const url = `https://api.twitterpicker.com/tweet/mediav2?id=${tweetId}`;
return GM_xmlhttpRequest({
method: "GET",
headers: {
"Content-Type": "application/json",
},
url,
onload: function (response) {
const json = JSON.parse(response.responseText);
console.debug("json", json);
const videos = json.media.videos;
if (videos.length) {
const variants = videos[0].variants;
// download the best quality mp4 variant by default
if (variants.length) {
const video = videos[0].variants.reduce((acc, cur) => {
if (
acc.content_type == "video/mp4" &&
cur.content_type == "video/mp4"
) {
return getVideoResolutionQuality(acc.url) >
getVideoResolutionQuality(cur.url)
? acc
: cur;
} else {
return acc.content_type == "video/mp4" ? acc : cur;
}
});
const link = document.createElement("a");
link.setAttribute("href", video.url);
link.setAttribute("download", `${tweetId}.mp4`);
link.target = "_blank";
console.debug("best quality video", video.url);
link.click();
}
}
},
});
}
// download button
const style = document.createElement("style");
style.innerHTML = `
.tweet-download-button {
display: flex;
align-items: center;
justify-content: center;
outline-style: none;
color: rgb(113,118,123);
background-color: rgba(0,0,0,0);
transition-duration: 0.2s;
transition-property: background-color,color, box-shadow;
}
.tweet-download-button:hover {
background-color: rgba(29,155,240,0.1);
color: rgb(29,155,240);
cursor: pointer;
}
`;
document.body.appendChild(style);
const regex = /^https:\/\/twitter\.com\/(\w+$|home$|\w+\/status\/\d+$)/;
const statusRegex = /^https:\/\/twitter\.com\/\w+\/status\/\d+$/;
const tweetSet = new WeakSet();
// watch new added Tweet cards
function watchTweetNodes(mutationList) {
mutationList.forEach(function (mutationRecord) {
mutationRecord.addedNodes.forEach(function (node) {
if (!node.querySelector || typeof node.querySelector != "function") {
return;
}
const tweet = node.querySelector(`article[data-testid="tweet"]`);
if (tweet == null) {
return;
}
// prevent duplication
if (tweetSet.has(tweet)) {
return;
}
let tweetId = "";
const tweetUser = tweet.querySelector(
`div[data-testid="User-Name"] a[aria-label]`
);
// timeline or conversation tweets
if (tweetUser) {
tweetId = tweetUser
.getAttribute("href")
.substring(1)
.split("/status/")
.at(1);
}
// status tweet
if (tweetId == "" && statusRegex.test(window.location.href)) {
tweetId = window.location.href.split("/status/").at(1);
}
const downloadButton = document.createElement("div");
downloadButton.dataset.id = tweetId;
downloadButton.classList.add("tweet-download-button");
downloadButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" viewBox="0 0 24 24" fill="none">
<path d="M11 5C11 4.44772 11.4477 4 12 4C12.5523 4 13 4.44772 13 5V12.1578L16.2428 8.91501L17.657 10.3292L12.0001 15.9861L6.34326 10.3292L7.75748 8.91501L11 12.1575V5Z" fill="currentColor"></path>
<path d="M4 14H6V18H18V14H20V18C20 19.1046 19.1046 20 18 20H6C4.89543 20 4 19.1046 4 18V14Z" fill="currentColor"></path>
</svg>
`;
downloadButton.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
await getTweetMedia(tweetId);
});
const groupRow = tweet.querySelector(`div[role="group"]`);
groupRow.appendChild(downloadButton);
tweetSet.add(tweet);
});
});
}
const observer = new MutationObserver(function (mutationList, observer) {
if (!regex.test(window.location.href)) {
return;
}
// add download button to new added tweet card
watchTweetNodes(mutationList);
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment