Twitter DL - Click "Always Allow"!

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

当前为 2023-07-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Twitter DL - Click "Always Allow"!
  3. // @version 1.0.5
  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. // @match https://twitter.com/*
  9. // @connect twitterpicker.com
  10. // @connect twimg.com
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  12. // @grant GM.xmlHttpRequest
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. let injectedTweets = [];
  17. const checkFrequency = 150; // in milliseconds
  18. const apiEndpoint = "https://api.twitterpicker.com/tweet/mediav2?id=";
  19. const downloadText = "Download"
  20.  
  21. const style =
  22. `.dl-video {
  23. padding: 6px;
  24. padding-left: 5px;
  25. padding-right: 5px;
  26. margin-left: 5px;
  27. margin-bottom: 2px;
  28. border-color: black;
  29. border-style: none;
  30. border-radius: 10px;
  31. color: white;
  32.  
  33. background-color: rgba(39, 39, 39, 0.46);
  34. font-family: Arial, Helvetica, sans-serif;
  35. font-size: xx-small;
  36.  
  37. cursor: pointer;
  38. }
  39.  
  40. .dl-hq {
  41. background-color: rgba(28, 199, 241, 0.46);
  42. }
  43. .dl-lq {
  44. background-color: rgba(185, 228, 138, 0.46);
  45. }`;
  46.  
  47. // Styles
  48. function injectStyles() {
  49. const styleElement = document.createElement("style");
  50. styleElement.textContent = style;
  51.  
  52. document.head.appendChild(styleElement);
  53. }
  54. injectStyles();
  55.  
  56. // Snippet extraction
  57. function getRetweetFrame(tweetElement) {
  58. let retweetFrame = null;
  59. const candidates = tweetElement.querySelectorAll(`[id^="id__"]`);
  60.  
  61. candidates.forEach((candidate) => {
  62. const candidateFrame = candidate.querySelector('div[tabindex="0"][role="link"]');
  63.  
  64. if (candidateFrame)
  65. retweetFrame = candidateFrame;
  66. });
  67.  
  68. return retweetFrame;
  69. }
  70. function getTopBar(tweetElement, isRetweet) {
  71. // I know its kind of bad but it works
  72.  
  73. let element = tweetElement;
  74.  
  75. if (isRetweet) {
  76. const retweetFrame = getRetweetFrame(tweetElement);
  77. const videoPlayer = tweetElement.querySelector('[data-testid="videoPlayer"]');
  78. const videoPlayerOnRetweet = retweetFrame.querySelector('[data-testid="videoPlayer"]')
  79.  
  80. const isVideoOnRetweet = (videoPlayer == videoPlayerOnRetweet);
  81. if (videoPlayerOnRetweet && isVideoOnRetweet) element = retweetFrame;
  82. else if (videoPlayerOnRetweet == null) element = tweetElement;
  83. }
  84.  
  85. const userName = element.querySelector('[data-testid="User-Name"]');
  86. if (isRetweet && element != tweetElement) return userName.parentNode.parentNode;
  87. return userName.parentNode.parentNode.parentNode;
  88. }
  89.  
  90. // Fetching
  91. async function getMediasFromTweetId(id) {
  92. const url = `${apiEndpoint}${id}`;
  93.  
  94. const request = await GM.xmlHttpRequest({
  95. method: "GET",
  96. url: url
  97. });
  98. const result = JSON.parse(request.responseText);
  99. let foundMedias = [];
  100. const medias = result.media;
  101.  
  102. if (medias) {
  103. const videos = medias.videos;
  104.  
  105. if (videos.length > 0) {
  106. for (let i = 0; i < videos.length; i++) {
  107. const video = videos[i];
  108. const variants = video.variants;
  109. if (!variants || variants.length == 0) continue;
  110.  
  111. // Check variant medias
  112. let videoContestants = {};
  113.  
  114. variants.forEach((variant) => {
  115. const isVideo = (variant.content_type.startsWith("video"));
  116.  
  117. if (isVideo) {
  118. const bitrate = variant.bitrate;
  119. const url = variant.url;
  120. videoContestants[url] = bitrate;
  121. };
  122. })
  123.  
  124. // Sort by lowest to highest bitrate
  125. const sortedContestants = Object.values(videoContestants).sort((a, b) => a - b);
  126. const findContestant = (value) => {
  127. const entry = Object.entries(videoContestants).find(([key, val]) => val === value);
  128. return entry ? entry[0] : null;
  129. };
  130.  
  131. let lowQualityVideo = null;
  132. let highQualityVideo = null;
  133.  
  134. for (let k = 0; k < sortedContestants.length; k++) {
  135. const bitrate = sortedContestants[k];
  136. const url = findContestant(bitrate);
  137.  
  138. if (url) {
  139. lowQualityVideo = findContestant(sortedContestants[0]);
  140.  
  141. if (sortedContestants.length > 1) // If has atleast 2 entries
  142. highQualityVideo = findContestant(sortedContestants[sortedContestants.length - 1]);
  143. }
  144. }
  145.  
  146. const meta = result.meta;
  147.  
  148. let mediaInformation = {
  149. "hq" : highQualityVideo,
  150. "lq" : lowQualityVideo,
  151. "metadata" : meta
  152. }
  153. foundMedias.push(mediaInformation);
  154. }
  155. }
  156. }
  157.  
  158. return foundMedias;
  159. }
  160.  
  161. // Downloading
  162. async function downloadFile(button, url, mode, filename) {
  163. const baseText = `${downloadText} (${mode.toUpperCase()})`;
  164. button.disabled = true;
  165. button.innerText = "Downloading...";
  166. console.log(`[TwitterDL] Downloading Tweet URL (${mode.toUpperCase()}): ${url}`);
  167. function finish() {
  168. if (button.innerText == baseText) return;
  169.  
  170. button.disabled = false;
  171. button.innerText = baseText;
  172. }
  173.  
  174. GM.xmlHttpRequest({
  175. method: 'GET',
  176. url: url,
  177. responseType: 'blob',
  178. onload: function(response) {
  179. const blob = response.response;
  180. const link = document.createElement('a');
  181.  
  182. link.href = URL.createObjectURL(blob);
  183. link.setAttribute('download', filename);
  184. link.click();
  185.  
  186. URL.revokeObjectURL(link.href);
  187. button.innerText = 'Downloaded!';
  188. button.disabled = false;
  189.  
  190. setTimeout(finish, 1000);
  191. },
  192. onerror: function(error) {
  193. console.error('[TwitterDL] Download Error:', error);
  194. button.innerText = 'Download Failed';
  195. setTimeout(finish, 1000);
  196. },
  197. onprogress: function(progressEvent) {
  198. if (progressEvent.lengthComputable) {
  199. const percentComplete = Math.round((progressEvent.loaded / progressEvent.total) * 100);
  200. button.innerText = `Downloading: ${percentComplete}%`;
  201. } else
  202. button.innerText = 'Downloading...';
  203. }
  204. });
  205. }
  206. function createDownloadButton(tweetId, tag) {
  207. const button = document.createElement("button");
  208. button.hidden = true;
  209.  
  210. getMediasFromTweetId(tweetId).then((mediaInformation) => {
  211. const video = mediaInformation[0];
  212. if (!video) return;
  213. const url = video[tag];
  214. const metadata = video.metadata;
  215. const username = metadata.username;
  216. const filename = `TwitterDL_${username}_${tweetId}`;
  217. console.log(filename);
  218.  
  219. button.classList.add("dl-video", `dl-${tag}`);
  220. button.innerText = `${downloadText} (${tag.toUpperCase()})`;
  221. button.setAttribute("href", url);
  222. button.setAttribute("download", "");
  223. button.addEventListener('click', async() => {
  224. await downloadFile(button, url, tag, filename);
  225. });
  226.  
  227. button.hidden = false;
  228. });
  229.  
  230. return button;
  231. }
  232. function createDownloadButtons(tweetElement) {
  233. const tweetInformation = getTweetInformation(tweetElement);
  234. if (!tweetInformation) return;
  235. const tweetId = tweetInformation.id;
  236. getMediasFromTweetId(tweetId).then((mediaInformation) => {
  237. const video = mediaInformation[0];
  238. if (!video) return;
  239.  
  240. const retweetFrame = getRetweetFrame(tweetElement);
  241. const isRetweet = (retweetFrame != null);
  242.  
  243. let lowQualityButton;
  244. let highQualityButton;
  245. if (video["lq"]) lowQualityButton = createDownloadButton(tweetId, "lq");
  246. if (video["hq"]) highQualityButton = createDownloadButton(tweetId, "hq");
  247. const videoPlayer = isRetweet ? tweetElement.querySelector('[data-testid="videoPlayer"]') : null;
  248. const videoPlayerOnRetweet = isRetweet ? retweetFrame.querySelector('[data-testid="videoPlayer"]') : null;
  249.  
  250. const topBar = getTopBar(tweetElement, isRetweet);
  251. const threeDotsElement = topBar.lastChild
  252.  
  253. const isVideoOnRetweet = (videoPlayer == videoPlayerOnRetweet);
  254. if (!lowQualityButton && !highQualityButton) return;
  255.  
  256. // Order: HQ then LQ
  257. if (videoPlayer != null && isRetweet && isVideoOnRetweet) {
  258. // Add a little side dot
  259. addSideTextToRetweet(tweetElement, " · ", 6, 5);
  260.  
  261. if (highQualityButton) topBar.appendChild(highQualityButton);
  262. if (lowQualityButton) topBar.appendChild(lowQualityButton);
  263. } else {
  264. if (lowQualityButton) topBar.insertBefore(lowQualityButton, threeDotsElement);
  265. if (highQualityButton) topBar.insertBefore(highQualityButton, lowQualityButton);
  266. }
  267. })
  268. }
  269. function addSideTextToRetweet(tweetElement, text, forcedMargin, forcedWidth) {
  270. const timeElement = tweetElement.querySelector("time");
  271. const computedStyles = window.getComputedStyle(timeElement);
  272.  
  273. // Make a new text based on the font and color
  274. const textElement = timeElement.cloneNode(true);
  275. textElement.innerText = text;
  276. textElement.setAttribute("datetime", "");
  277.  
  278. for (const property of computedStyles) {
  279. textElement.style[property] = computedStyles.getPropertyValue(property);
  280. }
  281.  
  282. textElement.style.overflow = "visible";
  283. textElement.style["padding-left"] = "4px";
  284. textElement.style["margin-left"] = forcedMargin || 0;
  285.  
  286. const tweetAvatarElement = tweetElement.querySelectorAll('[data-testid="Tweet-User-Avatar"]')[1];
  287. const targetTweetBar = tweetAvatarElement.parentNode;
  288.  
  289. targetTweetBar.appendChild(textElement);
  290.  
  291. const contentWidth = textElement.scrollWidth;
  292. textElement.style.width = (forcedWidth || contentWidth) + 'px';
  293. injectedFallbacks.push(tweetElement);
  294. }
  295.  
  296. // Page information gathering
  297. function getTweetsInPage() {
  298. return document.getElementsByTagName("article");
  299. }
  300. let injectedFallbacks = [];
  301. function getTweetInformation(tweetElement) {
  302. let information = {};
  303.  
  304. // ID
  305. // Check the tweet timestamp, it has a link with the id at the end
  306. // In case something goes wrong, a fallback text is shown
  307. let id = null;
  308.  
  309. const retweetFrame = getRetweetFrame(tweetElement);
  310. const isRetweet = (retweetFrame != null);
  311. const videoPlayer = isRetweet ? retweetFrame.querySelector('[data-testid="videoPlayer"]') : null;
  312.  
  313. const isPost = (isStatusUrl(window.location.href));
  314.  
  315. try {
  316. if (isRetweet && isPost) {
  317. const hasRetweetVideoPlayer = (videoPlayer != null);
  318. if (hasRetweetVideoPlayer)
  319. id = (window.location.href).split("/").pop();
  320. } else {
  321. const timeElement = tweetElement.querySelector("time");
  322. const timeHref = timeElement.parentNode;
  323. const tweetUrl = timeHref.href;
  324. id = tweetUrl.split("/").pop();
  325. }
  326. } catch (error) {
  327. try {
  328. if (injectedFallbacks.includes(tweetElement)) return;
  329.  
  330. const retweetFrame = getRetweetFrame(tweetElement);
  331. const videoPlayer = retweetFrame.querySelector('[data-testid="videoPlayer"]');
  332. if (!videoPlayer != null && retweetFrame != null && isStatusUrl(window.location.href)) return;
  333. console.log("[TwitterDL] Twitter quote retweets from statuses are not supported yet. Throwing fallback");
  334.  
  335. addSideTextToRetweet(tweetElement, " · Open to Download");
  336. } catch (error) {}
  337. }
  338.  
  339. if (!id) return;
  340. information.id = id;
  341.  
  342. // VideoPlayer element
  343. const videoPlayerElement = tweetElement.querySelector('[data-testid="videoPlayer"]');
  344. information.videoPlayer = videoPlayerElement;
  345.  
  346. // Play button
  347. return information;
  348. }
  349.  
  350. // Page injection
  351. async function injectAll() {
  352. const tweets = getTweetsInPage();
  353. for (let i = 0; i < tweets.length; i++) {
  354. const tweet = tweets[i];
  355. const alreadyInjected = injectedTweets.includes(tweet);
  356.  
  357. if (!alreadyInjected) {
  358. const videoPlayer = tweet.querySelector('[data-testid="videoPlayer"]');
  359. const isVideo = (videoPlayer != null);
  360. if (!isVideo) continue;
  361.  
  362. createDownloadButtons(tweet);
  363. injectedTweets.push(tweet);
  364. }
  365. }
  366. }
  367. function checkForInjection() {
  368. const tweets = getTweetsInPage();
  369. const shouldInject = (injectedTweets.length != tweets.length);
  370.  
  371. if (shouldInject) injectAll();
  372. }
  373.  
  374. function isStatusUrl(url) {
  375. const statusUrlRegex = /^https?:\/\/twitter\.com\/\w+\/status\/\d+$/;
  376. return statusUrlRegex.test(url);
  377. }
  378. function isValidUrl(url) {
  379. const tweetUrlRegex = /^https?:\/\/twitter\.com\/\w+(\/\w+)*$/ ;
  380. return tweetUrlRegex.test(url) || isStatusUrl(window.location.href);
  381. }
  382. if (isValidUrl(window.location.href)) {
  383. console.log("[TwitterDL] by (real)coloride - 2023 // Loading... ");
  384.  
  385. setInterval(async() => {
  386. try {
  387. checkForInjection();
  388. } catch (error) {
  389. console.error("[TwitterDL] Fatal error: ", error);
  390. }
  391. }, checkFrequency);
  392. }
  393. })();

QingJ © 2025

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