Twitter DL - Click "Always Allow"!

Download twitter videos directly from your browser! (CLICK "ALWAYS ALLOW" IF PROMPTED!)

  1. // ==UserScript==
  2. // @name Twitter DL - Click "Always Allow"!
  3. // @version 1.1.2
  4. // @description Download twitter videos directly from your browser! (CLICK "ALWAYS ALLOW" IF PROMPTED!)
  5. // @author realcoloride
  6. // @license MIT
  7. // @namespace https://twitter.com/*
  8. // @namespace https://x.com/*
  9. // @match https://twitter.com/*
  10. // @match https://x.com/*
  11. // @match https://pro.twitter.com/*
  12. // @match https://pro.x.com/*
  13. // @connect twitter-video-download.com
  14. // @connect twimg.com
  15. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  16. // @grant GM.xmlHttpRequest
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. let injectedTweets = [];
  21. const checkFrequency = 150; // in milliseconds
  22. const apiEndpoint = "https://twitter-video-download.com/fr/tweet/";
  23. const downloadText = "Download"
  24.  
  25. const style =
  26. `.dl-video {
  27. padding: 6px;
  28. padding-left: 5px;
  29. padding-right: 5px;
  30. margin-left: 5px;
  31. margin-bottom: 2px;
  32. border-color: black;
  33. border-style: none;
  34. border-radius: 10px;
  35. color: white;
  36.  
  37. background-color: rgba(39, 39, 39, 0.46);
  38. font-family: Arial, Helvetica, sans-serif;
  39. font-size: xx-small;
  40.  
  41. cursor: pointer;
  42. }
  43.  
  44. .dl-hq {
  45. background-color: rgba(28, 199, 241, 0.46);
  46. }
  47. .dl-lq {
  48. background-color: rgba(185, 228, 138, 0.46);
  49. }
  50. .dl-gif {
  51. background-color: rgba(219, 117, 22, 0.46);
  52. }
  53. `;
  54.  
  55. // Styles
  56. function injectStyles() {
  57. const styleElement = document.createElement("style");
  58. styleElement.textContent = style;
  59.  
  60. document.head.appendChild(styleElement);
  61. }
  62. injectStyles();
  63.  
  64. // Snippet extraction
  65. function getRetweetFrame(tweetElement) {
  66. let retweetFrame = null;
  67. const candidates = tweetElement.querySelectorAll(`[id^="id__"]`);
  68.  
  69. candidates.forEach((candidate) => {
  70. const candidateFrame = candidate.querySelector('div[tabindex="0"][role="link"]');
  71.  
  72. if (candidateFrame)
  73. retweetFrame = candidateFrame;
  74. });
  75.  
  76. return retweetFrame;
  77. }
  78. function getTopBar(tweetElement, isRetweet) {
  79. // I know its kind of bad but it works
  80.  
  81. let element = tweetElement;
  82.  
  83. if (isRetweet) {
  84. const retweetFrame = getRetweetFrame(tweetElement);
  85. const videoPlayer = tweetElement.querySelector('[data-testid="videoPlayer"]');
  86. const videoPlayerOnRetweet = retweetFrame.querySelector('[data-testid="videoPlayer"]')
  87.  
  88. const isVideoOnRetweet = (videoPlayer == videoPlayerOnRetweet);
  89.  
  90. if (videoPlayerOnRetweet && isVideoOnRetweet) element = retweetFrame;
  91. else if (videoPlayerOnRetweet == null) element = tweetElement;
  92. }
  93.  
  94. const userName = element.querySelector('[data-testid="User-Name"]');
  95.  
  96. if (isRetweet && element != tweetElement) return userName.parentNode.parentNode;
  97. return userName.parentNode.parentNode.parentNode;
  98. }
  99.  
  100. // Fetching
  101. async function getMediasFromTweetId(tweetInformation) {
  102. const id = tweetInformation.id;
  103.  
  104. const payload = {
  105. "url": `${apiEndpoint}${id}`,
  106. "headers": {
  107. "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
  108. "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
  109. "cache-control": "max-age=0",
  110. "sec-ch-ua": "\"Not/A)Brand\";v=\"99\", \"Google Chrome\";v=\"115\", \"Chromium\";v=\"115\"",
  111. "sec-ch-ua-mobile": "?0",
  112. "sec-ch-ua-platform": "\"Windows\"",
  113. "sec-fetch-dest": "document",
  114. "sec-fetch-mode": "navigate",
  115. "sec-fetch-site": "same-origin",
  116. "sec-fetch-user": "?1",
  117. "upgrade-insecure-requests": "1"
  118. },
  119. "referrer": "https://twitter-video-download.com/en",
  120. "referrerPolicy": "strict-origin-when-cross-origin",
  121. "body": null,
  122. "method": "GET",
  123. "mode": "cors",
  124. "credentials": "omit"
  125. };
  126. const request = await GM.xmlHttpRequest(payload);
  127.  
  128. let lq = null;
  129. let hq = null;
  130.  
  131. try {
  132. const regex = /https:\/\/[a-zA-Z0-9_-]+\.twimg\.com\/[a-zA-Z0-9_\-./]+\.mp4/g;
  133. const text = request.responseText;
  134. const links = text.match(regex);
  135.  
  136. // Calculate the size of a video based on resolution
  137. function calculateSize(resolution) {
  138. const parts = resolution.split("x");
  139. const width = parseInt(parts[0]);
  140. const height = parseInt(parts[1]);
  141. return width * height;
  142. }
  143.  
  144. if (!links) return null;
  145.  
  146. // Map links to objects with resolution and size
  147. const linkObjects = links.map(link => {
  148. const resolutionMatch = link.match(/\/(\d+x\d+)\//);
  149. const resolution = resolutionMatch ? resolutionMatch[1] : "";
  150. const size = calculateSize(resolution);
  151. return { link, resolution, size };
  152. });
  153.  
  154. // Sort linkObjects based on size in descending order
  155. linkObjects.sort((a, b) => a.size - b.size);
  156.  
  157. // Create a Set to track seen links and store unique links
  158. const uniqueLinks = new Set();
  159. const deduplicatedLinks = [];
  160.  
  161. for (const obj of linkObjects) {
  162. if (!uniqueLinks.has(obj.link)) {
  163. uniqueLinks.add(obj.link);
  164. deduplicatedLinks.push(obj.link);
  165. }
  166. }
  167.  
  168. if (tweetInformation.isGif && tweetInformation.tabIndex == "-1" ||
  169. links[0].startsWith('https://video.twimg.com/tweet_video/')
  170. ) {
  171. lq = links[0];
  172. } else {
  173. lq = deduplicatedLinks[0];
  174.  
  175. if (deduplicatedLinks.length > 1) hq = deduplicatedLinks[deduplicatedLinks.length-1];
  176. // first quality is VERY bad so if can swap to second (medium) then its better
  177. if (deduplicatedLinks.length > 2) lq = deduplicatedLinks[1];
  178. }
  179. } catch (error) {
  180. console.error(error);
  181. return null;
  182. }
  183.  
  184. return {lq, hq};
  185. }
  186.  
  187. // Downloading
  188. async function downloadFile(button, url, mode, filename) {
  189. const baseText = `${downloadText} (${mode.toUpperCase()})`;
  190.  
  191. button.disabled = true;
  192. button.innerText = "Downloading...";
  193.  
  194. console.log(`[TwitterDL] Downloading Tweet URL (${mode.toUpperCase()}): ${url}`);
  195.  
  196. function finish() {
  197. if (button.innerText == baseText) return;
  198.  
  199. button.disabled = false;
  200. button.innerText = baseText;
  201. }
  202.  
  203. GM.xmlHttpRequest({
  204. method: 'GET',
  205. url: url,
  206. responseType: 'blob',
  207. onload: function(response) {
  208. const blob = response.response;
  209. const link = document.createElement('a');
  210.  
  211. link.href = URL.createObjectURL(blob);
  212. link.setAttribute('download', filename);
  213. link.click();
  214.  
  215. URL.revokeObjectURL(link.href);
  216. button.innerText = 'Downloaded!';
  217. button.disabled = false;
  218.  
  219. setTimeout(finish, 1000);
  220. },
  221. onerror: function(error) {
  222. console.error('[TwitterDL] Download Error:', error);
  223. button.innerText = 'Download Failed';
  224.  
  225. setTimeout(finish, 1000);
  226. },
  227. onprogress: function(progressEvent) {
  228. if (progressEvent.lengthComputable) {
  229. const percentComplete = Math.round((progressEvent.loaded / progressEvent.total) * 100);
  230. button.innerText = `Downloading: ${percentComplete}%`;
  231. } else
  232. button.innerText = 'Downloading...';
  233. }
  234. });
  235. }
  236. function createDownloadButton(tweetInformation, url, tag) {
  237. const button = document.createElement("button");
  238. button.hidden = true;
  239.  
  240. const username = tweetInformation.username;
  241. const filename = `TwitterDL_${username}_${tweetInformation.id}`;
  242.  
  243. button.classList.add("dl-video", `dl-${tag}`);
  244. button.innerText = `${downloadText} (${tag.toUpperCase()})`;
  245. button.setAttribute("href", url);
  246. button.setAttribute("download", "");
  247. button.addEventListener('click', async() => {
  248. await downloadFile(button, url, tag, filename);
  249. });
  250.  
  251. button.hidden = false;
  252.  
  253. return button;
  254. }
  255. function createDownloadButtons(tweetElement) {
  256. const tweetInformation = getTweetInformation(tweetElement);
  257. if (!tweetInformation) return;
  258.  
  259. getMediasFromTweetId(tweetInformation).then((medias) => {
  260. if (!medias) return;
  261.  
  262. const retweetFrame = getRetweetFrame(tweetElement);
  263. const isRetweet = (retweetFrame != null);
  264.  
  265. let lowQualityButton;
  266. let highQualityButton;
  267.  
  268. const lq = medias.lq;
  269. const hq = medias.hq;
  270.  
  271. if (lq) lowQualityButton = createDownloadButton(tweetInformation, lq, tweetInformation.isGif ? "gif" : "lq");
  272. if (hq && !tweetInformation.isGif) highQualityButton = createDownloadButton(tweetInformation, hq, "hq");
  273.  
  274. const videoPlayer = isRetweet ? tweetElement.querySelector('[data-testid="videoPlayer"]') : null;
  275. const videoPlayerOnRetweet = isRetweet ? retweetFrame.querySelector('[data-testid="videoPlayer"]') : null;
  276.  
  277. const topBar = getTopBar(tweetElement, isRetweet);
  278. const threeDotsElement = topBar.lastChild
  279.  
  280. const isVideoOnRetweet = (videoPlayer == videoPlayerOnRetweet);
  281.  
  282. if (!lowQualityButton && !highQualityButton) return;
  283.  
  284. // Order: HQ then LQ
  285. if (videoPlayer != null && isRetweet && isVideoOnRetweet) {
  286. // Add a little side dot
  287. addSideTextToRetweet(tweetElement, " · ", 6, 5);
  288.  
  289. if (highQualityButton) topBar.appendChild(highQualityButton);
  290. if (lowQualityButton) topBar.appendChild(lowQualityButton);
  291. } else {
  292. if (lowQualityButton) topBar.insertBefore(lowQualityButton, threeDotsElement);
  293. if (highQualityButton) topBar.insertBefore(highQualityButton, lowQualityButton);
  294. }
  295. })
  296. }
  297. function addSideTextToRetweet(tweetElement, text, forcedMargin, forcedWidth) {
  298. const timeElement = tweetElement.querySelector("time");
  299. const computedStyles = window.getComputedStyle(timeElement);
  300.  
  301. // Make a new text based on the font and color
  302. const textElement = timeElement.cloneNode(true);
  303. textElement.innerText = text;
  304. textElement.setAttribute("datetime", "");
  305.  
  306. for (const property of computedStyles) {
  307. textElement.style[property] = computedStyles.getPropertyValue(property);
  308. }
  309.  
  310. textElement.style.overflow = "visible";
  311. textElement.style["padding-left"] = "4px";
  312. textElement.style["margin-left"] = forcedMargin || 0;
  313.  
  314. const tweetAvatarElement = tweetElement.querySelectorAll('[data-testid="Tweet-User-Avatar"]')[1];
  315. const targetTweetBar = tweetAvatarElement.parentNode;
  316.  
  317. targetTweetBar.appendChild(textElement);
  318.  
  319. const contentWidth = textElement.scrollWidth;
  320. textElement.style.width = (forcedWidth || contentWidth) + 'px';
  321.  
  322. injectedFallbacks.push(tweetElement);
  323. }
  324.  
  325. // Page information gathering
  326. function getTweetsInPage() {
  327. return document.getElementsByTagName("article");
  328. }
  329. let injectedFallbacks = [];
  330. function getTweetInformation(tweetElement) {
  331. let information = {};
  332.  
  333. // ID
  334. // Check the tweet timestamp, it has a link with the id at the end
  335. // In case something goes wrong, a fallback text is shown
  336. let id = null;
  337. let username = null;
  338. let tweetUrl = null;
  339. let isGif = false;
  340. let tabIndex = null;
  341.  
  342. const retweetFrame = getRetweetFrame(tweetElement);
  343. const isRetweet = (retweetFrame != null);
  344.  
  345. const videoPlayer = isRetweet ? retweetFrame.querySelector('[data-testid="videoPlayer"]') : null;
  346.  
  347. const isPost = (isStatusUrl(window.location.href));
  348.  
  349. tabIndex = tweetElement.getAttribute('tabindex');
  350.  
  351. const regex = /https:\/\/(?:pro\.)?x\.com\/([^\/]+)\/status\/(\d+)/;
  352. function setInfo(url) {
  353. const match = url.match(regex);
  354.  
  355. id = match[2];
  356. username = match[1];
  357. tweetUrl = url;
  358. }
  359.  
  360. const url = window.location.href;
  361.  
  362. try {
  363. setInfo(url);
  364. } catch {}
  365.  
  366. function fallback(reason) {
  367. if (injectedFallbacks.includes(tweetElement)) return;
  368.  
  369. console.log("[TwitterDL] Twitter quote retweets from statuses are not supported yet, sorry! Throwing fallback... \nScope: " + reason);
  370.  
  371. addSideTextToRetweet(tweetElement, " · Open to Download");
  372. }
  373.  
  374. try {
  375. if (isRetweet) {
  376. if (isPost) {
  377. const hasRetweetVideoPlayer = (videoPlayer != null);
  378. if (hasRetweetVideoPlayer)
  379. fallback("isretweet, ispost, hasretweetvideoplayer");
  380. } else fallback("isretweet");
  381. } else {
  382. const timeElement = tweetElement.querySelector("time");
  383. const timeHref = timeElement.parentNode;
  384. const tweetUrl = timeHref.href;
  385.  
  386. if (tweetUrl) setInfo(tweetUrl);
  387. else fallback("no time info");
  388. }
  389. } catch (error) {
  390. fallback("internal error: " + error);
  391. console.error(error);
  392. }
  393.  
  394. // VideoPlayer element
  395. const videoPlayerElement = tweetElement.querySelector('[data-testid="videoPlayer"]');
  396. const spanElement = videoPlayerElement.querySelector('div[dir="ltr"] > span');
  397.  
  398. if (spanElement)
  399. isGif = spanElement.innerText == "GIF";
  400.  
  401. if (!id) return;
  402. information.id = id;
  403. information.username = username;
  404. information.url = tweetUrl;
  405. information.videoPlayer = videoPlayerElement;
  406. information.isGif = isGif;
  407. information.tabIndex = tabIndex;
  408.  
  409. // Play button
  410. return information;
  411. }
  412.  
  413. // Page injection
  414. async function injectAll() {
  415. const tweets = getTweetsInPage();
  416. for (let i = 0; i < tweets.length; i++) {
  417. const tweet = tweets[i];
  418. const alreadyInjected = injectedTweets.includes(tweet);
  419.  
  420. if (!alreadyInjected) {
  421. const videoPlayer = tweet.querySelector('[data-testid="videoPlayer"]');
  422. const isVideo = (videoPlayer != null);
  423.  
  424. if (!isVideo) continue;
  425.  
  426. createDownloadButtons(tweet);
  427. injectedTweets.push(tweet);
  428. }
  429. }
  430. }
  431. function checkForInjection() {
  432. const tweets = getTweetsInPage();
  433. const shouldInject = (injectedTweets.length != tweets.length);
  434.  
  435. if (shouldInject) injectAll();
  436. }
  437.  
  438. function isStatusUrl(url) {
  439. const statusUrlRegex = /^https?:\/\/(pro\.x|x)\.com\/\w+\/status\/\d+$/;
  440. return statusUrlRegex.test(url);
  441. }
  442. function isValidUrl(url) {
  443. const tweetUrlRegex = /^https?:\/\/(pro\.x|x)\.com\/\w+(\/\w+)*$/;
  444. return tweetUrlRegex.test(url) || isStatusUrl(window.location.href);
  445. }
  446. if (isValidUrl(window.location.href)) {
  447. console.log("[TwitterDL] by (real)coloride - 2023 // Loading... ");
  448.  
  449. setInterval(async() => {
  450. try {
  451. checkForInjection();
  452. } catch (error) {
  453. console.error("[TwitterDL] Fatal error: ", error);
  454. }
  455. }, checkFrequency);
  456. }
  457. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址