您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download twitter videos directly from your browser! (CLICK "ALWAYS ALLOW" IF PROMPTED!)
当前为
- // ==UserScript==
- // @name Twitter DL - Click "Always Allow"!
- // @version 1.0.5
- // @description Download twitter videos directly from your browser! (CLICK "ALWAYS ALLOW" IF PROMPTED!)
- // @author realcoloride
- // @license MIT
- // @namespace https://twitter.com/*
- // @match https://twitter.com/*
- // @connect twitterpicker.com
- // @connect twimg.com
- // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
- // @grant GM.xmlHttpRequest
- // ==/UserScript==
- (function() {
- let injectedTweets = [];
- const checkFrequency = 150; // in milliseconds
- const apiEndpoint = "https://api.twitterpicker.com/tweet/mediav2?id=";
- const downloadText = "Download"
- const style =
- `.dl-video {
- padding: 6px;
- padding-left: 5px;
- padding-right: 5px;
- margin-left: 5px;
- margin-bottom: 2px;
- border-color: black;
- border-style: none;
- border-radius: 10px;
- color: white;
- background-color: rgba(39, 39, 39, 0.46);
- font-family: Arial, Helvetica, sans-serif;
- font-size: xx-small;
- cursor: pointer;
- }
- .dl-hq {
- background-color: rgba(28, 199, 241, 0.46);
- }
- .dl-lq {
- background-color: rgba(185, 228, 138, 0.46);
- }`;
- // Styles
- function injectStyles() {
- const styleElement = document.createElement("style");
- styleElement.textContent = style;
- document.head.appendChild(styleElement);
- }
- injectStyles();
- // Snippet extraction
- function getRetweetFrame(tweetElement) {
- let retweetFrame = null;
- const candidates = tweetElement.querySelectorAll(`[id^="id__"]`);
- candidates.forEach((candidate) => {
- const candidateFrame = candidate.querySelector('div[tabindex="0"][role="link"]');
- if (candidateFrame)
- retweetFrame = candidateFrame;
- });
- return retweetFrame;
- }
- function getTopBar(tweetElement, isRetweet) {
- // I know its kind of bad but it works
- let element = tweetElement;
- if (isRetweet) {
- const retweetFrame = getRetweetFrame(tweetElement);
- const videoPlayer = tweetElement.querySelector('[data-testid="videoPlayer"]');
- const videoPlayerOnRetweet = retweetFrame.querySelector('[data-testid="videoPlayer"]')
- const isVideoOnRetweet = (videoPlayer == videoPlayerOnRetweet);
- if (videoPlayerOnRetweet && isVideoOnRetweet) element = retweetFrame;
- else if (videoPlayerOnRetweet == null) element = tweetElement;
- }
- const userName = element.querySelector('[data-testid="User-Name"]');
- if (isRetweet && element != tweetElement) return userName.parentNode.parentNode;
- return userName.parentNode.parentNode.parentNode;
- }
- // Fetching
- async function getMediasFromTweetId(id) {
- const url = `${apiEndpoint}${id}`;
- const request = await GM.xmlHttpRequest({
- method: "GET",
- url: url
- });
- const result = JSON.parse(request.responseText);
- let foundMedias = [];
- const medias = result.media;
- if (medias) {
- const videos = medias.videos;
- if (videos.length > 0) {
- for (let i = 0; i < videos.length; i++) {
- const video = videos[i];
- const variants = video.variants;
- if (!variants || variants.length == 0) continue;
- // Check variant medias
- let videoContestants = {};
- variants.forEach((variant) => {
- const isVideo = (variant.content_type.startsWith("video"));
- if (isVideo) {
- const bitrate = variant.bitrate;
- const url = variant.url;
- videoContestants[url] = bitrate;
- };
- })
- // Sort by lowest to highest bitrate
- const sortedContestants = Object.values(videoContestants).sort((a, b) => a - b);
- const findContestant = (value) => {
- const entry = Object.entries(videoContestants).find(([key, val]) => val === value);
- return entry ? entry[0] : null;
- };
- let lowQualityVideo = null;
- let highQualityVideo = null;
- for (let k = 0; k < sortedContestants.length; k++) {
- const bitrate = sortedContestants[k];
- const url = findContestant(bitrate);
- if (url) {
- lowQualityVideo = findContestant(sortedContestants[0]);
- if (sortedContestants.length > 1) // If has atleast 2 entries
- highQualityVideo = findContestant(sortedContestants[sortedContestants.length - 1]);
- }
- }
- const meta = result.meta;
- let mediaInformation = {
- "hq" : highQualityVideo,
- "lq" : lowQualityVideo,
- "metadata" : meta
- }
- foundMedias.push(mediaInformation);
- }
- }
- }
- return foundMedias;
- }
- // Downloading
- async function downloadFile(button, url, mode, filename) {
- const baseText = `${downloadText} (${mode.toUpperCase()})`;
- button.disabled = true;
- button.innerText = "Downloading...";
- console.log(`[TwitterDL] Downloading Tweet URL (${mode.toUpperCase()}): ${url}`);
- function finish() {
- if (button.innerText == baseText) return;
- button.disabled = false;
- button.innerText = baseText;
- }
- GM.xmlHttpRequest({
- method: 'GET',
- url: url,
- responseType: 'blob',
- onload: function(response) {
- const blob = response.response;
- const link = document.createElement('a');
- link.href = URL.createObjectURL(blob);
- link.setAttribute('download', filename);
- link.click();
- URL.revokeObjectURL(link.href);
- button.innerText = 'Downloaded!';
- button.disabled = false;
- setTimeout(finish, 1000);
- },
- onerror: function(error) {
- console.error('[TwitterDL] Download Error:', error);
- button.innerText = 'Download Failed';
- setTimeout(finish, 1000);
- },
- onprogress: function(progressEvent) {
- if (progressEvent.lengthComputable) {
- const percentComplete = Math.round((progressEvent.loaded / progressEvent.total) * 100);
- button.innerText = `Downloading: ${percentComplete}%`;
- } else
- button.innerText = 'Downloading...';
- }
- });
- }
- function createDownloadButton(tweetId, tag) {
- const button = document.createElement("button");
- button.hidden = true;
- getMediasFromTweetId(tweetId).then((mediaInformation) => {
- const video = mediaInformation[0];
- if (!video) return;
- const url = video[tag];
- const metadata = video.metadata;
- const username = metadata.username;
- const filename = `TwitterDL_${username}_${tweetId}`;
- console.log(filename);
- button.classList.add("dl-video", `dl-${tag}`);
- button.innerText = `${downloadText} (${tag.toUpperCase()})`;
- button.setAttribute("href", url);
- button.setAttribute("download", "");
- button.addEventListener('click', async() => {
- await downloadFile(button, url, tag, filename);
- });
- button.hidden = false;
- });
- return button;
- }
- function createDownloadButtons(tweetElement) {
- const tweetInformation = getTweetInformation(tweetElement);
- if (!tweetInformation) return;
- const tweetId = tweetInformation.id;
- getMediasFromTweetId(tweetId).then((mediaInformation) => {
- const video = mediaInformation[0];
- if (!video) return;
- const retweetFrame = getRetweetFrame(tweetElement);
- const isRetweet = (retweetFrame != null);
- let lowQualityButton;
- let highQualityButton;
- if (video["lq"]) lowQualityButton = createDownloadButton(tweetId, "lq");
- if (video["hq"]) highQualityButton = createDownloadButton(tweetId, "hq");
- const videoPlayer = isRetweet ? tweetElement.querySelector('[data-testid="videoPlayer"]') : null;
- const videoPlayerOnRetweet = isRetweet ? retweetFrame.querySelector('[data-testid="videoPlayer"]') : null;
- const topBar = getTopBar(tweetElement, isRetweet);
- const threeDotsElement = topBar.lastChild
- const isVideoOnRetweet = (videoPlayer == videoPlayerOnRetweet);
- if (!lowQualityButton && !highQualityButton) return;
- // Order: HQ then LQ
- if (videoPlayer != null && isRetweet && isVideoOnRetweet) {
- // Add a little side dot
- addSideTextToRetweet(tweetElement, " · ", 6, 5);
- if (highQualityButton) topBar.appendChild(highQualityButton);
- if (lowQualityButton) topBar.appendChild(lowQualityButton);
- } else {
- if (lowQualityButton) topBar.insertBefore(lowQualityButton, threeDotsElement);
- if (highQualityButton) topBar.insertBefore(highQualityButton, lowQualityButton);
- }
- })
- }
- function addSideTextToRetweet(tweetElement, text, forcedMargin, forcedWidth) {
- const timeElement = tweetElement.querySelector("time");
- const computedStyles = window.getComputedStyle(timeElement);
- // Make a new text based on the font and color
- const textElement = timeElement.cloneNode(true);
- textElement.innerText = text;
- textElement.setAttribute("datetime", "");
- for (const property of computedStyles) {
- textElement.style[property] = computedStyles.getPropertyValue(property);
- }
- textElement.style.overflow = "visible";
- textElement.style["padding-left"] = "4px";
- textElement.style["margin-left"] = forcedMargin || 0;
- const tweetAvatarElement = tweetElement.querySelectorAll('[data-testid="Tweet-User-Avatar"]')[1];
- const targetTweetBar = tweetAvatarElement.parentNode;
- targetTweetBar.appendChild(textElement);
- const contentWidth = textElement.scrollWidth;
- textElement.style.width = (forcedWidth || contentWidth) + 'px';
- injectedFallbacks.push(tweetElement);
- }
- // Page information gathering
- function getTweetsInPage() {
- return document.getElementsByTagName("article");
- }
- let injectedFallbacks = [];
- function getTweetInformation(tweetElement) {
- let information = {};
- // ID
- // Check the tweet timestamp, it has a link with the id at the end
- // In case something goes wrong, a fallback text is shown
- let id = null;
- const retweetFrame = getRetweetFrame(tweetElement);
- const isRetweet = (retweetFrame != null);
- const videoPlayer = isRetweet ? retweetFrame.querySelector('[data-testid="videoPlayer"]') : null;
- const isPost = (isStatusUrl(window.location.href));
- try {
- if (isRetweet && isPost) {
- const hasRetweetVideoPlayer = (videoPlayer != null);
- if (hasRetweetVideoPlayer)
- id = (window.location.href).split("/").pop();
- } else {
- const timeElement = tweetElement.querySelector("time");
- const timeHref = timeElement.parentNode;
- const tweetUrl = timeHref.href;
- id = tweetUrl.split("/").pop();
- }
- } catch (error) {
- try {
- if (injectedFallbacks.includes(tweetElement)) return;
- const retweetFrame = getRetweetFrame(tweetElement);
- const videoPlayer = retweetFrame.querySelector('[data-testid="videoPlayer"]');
- if (!videoPlayer != null && retweetFrame != null && isStatusUrl(window.location.href)) return;
- console.log("[TwitterDL] Twitter quote retweets from statuses are not supported yet. Throwing fallback");
- addSideTextToRetweet(tweetElement, " · Open to Download");
- } catch (error) {}
- }
- if (!id) return;
- information.id = id;
- // VideoPlayer element
- const videoPlayerElement = tweetElement.querySelector('[data-testid="videoPlayer"]');
- information.videoPlayer = videoPlayerElement;
- // Play button
- return information;
- }
- // Page injection
- async function injectAll() {
- const tweets = getTweetsInPage();
- for (let i = 0; i < tweets.length; i++) {
- const tweet = tweets[i];
- const alreadyInjected = injectedTweets.includes(tweet);
- if (!alreadyInjected) {
- const videoPlayer = tweet.querySelector('[data-testid="videoPlayer"]');
- const isVideo = (videoPlayer != null);
- if (!isVideo) continue;
- createDownloadButtons(tweet);
- injectedTweets.push(tweet);
- }
- }
- }
- function checkForInjection() {
- const tweets = getTweetsInPage();
- const shouldInject = (injectedTweets.length != tweets.length);
- if (shouldInject) injectAll();
- }
- function isStatusUrl(url) {
- const statusUrlRegex = /^https?:\/\/twitter\.com\/\w+\/status\/\d+$/;
- return statusUrlRegex.test(url);
- }
- function isValidUrl(url) {
- const tweetUrlRegex = /^https?:\/\/twitter\.com\/\w+(\/\w+)*$/ ;
- return tweetUrlRegex.test(url) || isStatusUrl(window.location.href);
- }
- if (isValidUrl(window.location.href)) {
- console.log("[TwitterDL] by (real)coloride - 2023 // Loading... ");
- setInterval(async() => {
- try {
- checkForInjection();
- } catch (error) {
- console.error("[TwitterDL] Fatal error: ", error);
- }
- }, checkFrequency);
- }
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址