Twitter Video Downloader

Download Twitter videos via Twittervid website

  1. // ==UserScript==
  2. // @name Twitter Video Downloader
  3. // @version 1.0.2
  4. // @description Download Twitter videos via Twittervid website
  5. // @author w4t3r1ily
  6. // @namespace https://github.com/HayaoGai
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=twittervid.com
  8. // @include https://twitter.com/*
  9. // @match https://twitter.com/*
  10. // @include https://x.com/*
  11. // @match https://x.com/*
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15.  
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. // icons made by https://www.flaticon.com/authors/freepik
  21. const svg =
  22. `<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>`;
  23.  
  24. const resource = "https://twittervid.com/";
  25. let currentUrl = document.location.href;
  26. let updating = false;
  27.  
  28. init(10);
  29.  
  30. locationChange();
  31.  
  32. window.addEventListener("scroll", update);
  33.  
  34. function init(times) {
  35. for (let i = 0; i < times; i++) {
  36. setTimeout(findVideo1, 500 * i);
  37. setTimeout(findVideo2, 500 * i);
  38. setTimeout(sensitiveContent, 500 * i);
  39. }
  40. }
  41.  
  42. function findVideo1() {
  43. // video play button
  44. document
  45. .querySelectorAll("[data-testid='playButton']")
  46. .forEach(button => {
  47. // thumbnail
  48. button.parentElement
  49. .querySelectorAll("img:not(.download-set)")
  50. .forEach(thumbnail => {
  51. thumbnail.classList.add("download-set");
  52. const url = thumbnail.src;
  53. situation(url, thumbnail);
  54. });
  55. });
  56. }
  57.  
  58. function findVideo2() {
  59. // video
  60. document
  61. .querySelectorAll("video:not(.download-set)")
  62. .forEach(video => {
  63. video.classList.add("download-set");
  64. const url = video.poster;
  65. situation(url, video);
  66. });
  67. }
  68.  
  69. function situation(url, video) {
  70. // situation 1: gif
  71. if (url.includes("tweet_")) findMenu(video, "gif");
  72. // situation 2: video
  73. else if (
  74. url.includes("ext_tw_") ||
  75. url.includes("amplify_") ||
  76. url.includes("media")
  77. )
  78. findMenu(video, "video");
  79. // situation 3: unknown
  80. else console.log("Error: Unknown");
  81. }
  82.  
  83. function findMenu(child, type) {
  84. const article = child.closest("article:not(.article-set)");
  85. if (!article) return;
  86. article.classList.add("article-set");
  87. const menus = article.querySelectorAll("[data-testid='caret']");
  88. menus.forEach(menu =>
  89. menu.addEventListener("click", () => {
  90. clickMenu(article, type, false);
  91. if (type === "gif") clickMenu(article, type, true);
  92. })
  93. );
  94. }
  95.  
  96. function clickMenu(article, type, convert) {
  97. // check exist.
  98. if (!!document.querySelector(`.option-download-${convert}-set`)) return;
  99. // wait menu.
  100. if (!document.querySelector("[role='menuitem']")) {
  101. setTimeout(() => clickMenu(article, type, convert), 100);
  102. return;
  103. }
  104. const menu = document.querySelector("[role='menuitem']").parentElement;
  105. // add "download" option.
  106. const option = document.createElement("div");
  107. option.className =
  108. "css-1dbjc4n r-1loqt21 r-18u37iz r-1ny4l3l r-ymttw5 r-1yzf0co r-o7ynqc r-6416eg r-13qz1uu option-download-set";
  109. option.addEventListener("mouseenter", () =>
  110. option.classList.add(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"]))
  111. );
  112. option.addEventListener("mouseleave", () =>
  113. option.classList.remove(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"]))
  114. );
  115. option.addEventListener("click", () =>
  116. clickDownload(article, type, convert)
  117. );
  118. // icon
  119. const icon = document.createElement("div");
  120. icon.className = "css-1dbjc4n r-1777fci";
  121. icon.innerHTML = svg;
  122. const svgElement = icon.querySelector("svg");
  123. svgElement.setAttribute(
  124. "class",
  125. "r-4qtqp9 r-yyyyoo r-1q142lx r-1xvli5t r-zso239 r-dnmrzs r-bnwqim r-1plcrui r-lrvibr"
  126. );
  127. svgElement.classList.add(getTheme(["r-1re7ezh", "r-9ilb82", "r-111h2gw"]));
  128. // text
  129. const text1 = document.createElement("div");
  130. text1.className = "css-1dbjc4n r-16y2uox r-1wbh5a2";
  131.  
  132. const text2 = document.createElement("div");
  133. text2.className =
  134. "css-901oao r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0";
  135. text2.classList.add(getTheme(["r-hkyrab", "r-1fmj7o5", "r-jwli3a"]));
  136. const text3 = document.createElement("span");
  137. text3.className =
  138. "css-901oao css-16my406 r-1qd0xha r-ad9z0x r-bcqeeo r-qvutc0";
  139. text3.innerText = getLocalization(type, convert);
  140. // append
  141. menu.appendChild(option);
  142. option.appendChild(icon);
  143. option.appendChild(text1);
  144. text1.appendChild(text2);
  145. text2.appendChild(text3);
  146. }
  147.  
  148. function clickDownload(article, type, convert) {
  149. // gif
  150. if (type === "gif" && !convert) {
  151. let link;
  152. // condition 1: not play yet.
  153. article.querySelectorAll("video").forEach(video => {
  154. link = video.src;
  155. });
  156. // condition 2: playing.
  157. if (!link) {
  158. const image = [...article.querySelectorAll("img")].find(image =>
  159. image.src.includes("video")
  160. );
  161. const id = image.src.split(/[/?]/)[4];
  162. link = `https://video.twimg.com/tweet_video/${id}.mp4`;
  163. }
  164. // open
  165. window.open(link);
  166. }
  167. // video
  168. else {
  169. const tweetId = article.querySelector("time").parentElement.href.split('/').pop();
  170. window.open(`https://twittervid.com/i/status/${tweetId}`);
  171. }
  172. }
  173.  
  174. // Rest of the script remains unchanged...
  175.  
  176. function getTheme(array) {
  177. const body = document.querySelector("body");
  178. const color = body.style.backgroundColor; // "#74818e"
  179. const red = color.match(/\d+/)[0]; // "#74818e"
  180. switch (red) {
  181. case "255":
  182. return array[0]; // #74818e
  183. case "0":
  184. return array[1]; // #74818e
  185. default:
  186. return array[2]; // #74818e
  187. }
  188. }
  189.  
  190. function getLocalization(type, convert) {
  191. let download = "Download";
  192. switch (document.querySelector("html").lang) {
  193. case "zh-Hant":
  194. download = "下載";
  195. break;
  196. case "zh":
  197. download = "下载";
  198. break;
  199. case "ja":
  200. download = "ダウンロード";
  201. break;
  202. case "ko":
  203. download = "다운로드";
  204. break;
  205. case "ru":
  206. download = "Скачать";
  207. break;
  208. }
  209.  
  210. let extension = "";
  211. if (type === "gif") extension = convert ? " GIF" : " MP4";
  212.  
  213. return `${download}${extension}`;
  214. }
  215.  
  216. function sensitiveContent() {
  217. // click "view" button on sensitive content warning to run this script again.
  218. document.querySelectorAll(".r-42olwf.r-1vsu8ta:not(.view-set)").forEach(view => {
  219. view.classList.add("view-set");
  220. view.addEventListener("click", () => init(3));
  221. });
  222. }
  223.  
  224. function update() {
  225. if (updating) return;
  226. updating = true;
  227. init(3);
  228. setTimeout(() => { updating = false; }, 1000);
  229. }
  230.  
  231. function locationChange() {
  232. const observer = new MutationObserver(mutations => {
  233. mutations.forEach(() => {
  234. if (currentUrl !== document.location.href) {
  235. currentUrl = document.location.href;
  236. init(10);
  237. }
  238. });
  239. });
  240. const target = document.body;
  241. const config = { childList: true, subtree: true };
  242. observer.observe(target, config);
  243. }
  244.  
  245. })();
  246.  

QingJ © 2025

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