Greasy Fork镜像 支持简体中文。

Twitter 下載影片

即按前往下載影片的網頁。

  1. // ==UserScript==
  2. // @name Twitter: Download Video
  3. // @name:zh-TW Twitter 下載影片
  4. // @name:zh-CN Twitter 下载视频
  5. // @name:ja Twitter ビデオをダウンロード
  6. // @name:ko Twitter 비디오 다운로드
  7. // @name:ru Twitter Скачать видео
  8. // @version 1.0.7
  9. // @description One button click to direct video download web page.
  10. // @description:zh-TW 即按前往下載影片的網頁。
  11. // @description:zh-CN 一键导向下载视频的网页。
  12. // @description:ja ボタンをクリックして、ビデオのダウンロードWebページに移動します。
  13. // @description:ko 한 번의 클릭으로 비디오 다운로드 웹 사이트를 탐색하십시오.
  14. // @description:ru Нажмите кнопку, чтобы перейти на страницу загрузки видео.
  15. // @author Hayao-Gai
  16. // @namespace https://github.com/HayaoGai
  17. // @icon https://i.imgur.com/M9oO8K9.png
  18. // @include https://twitter.com/*
  19. // @include https://mobile.twitter.com/*
  20. // @grant none
  21. // ==/UserScript==
  22.  
  23. /* jshint esversion: 6 */
  24.  
  25. (function() {
  26. 'use strict';
  27.  
  28. // icons made by https://www.flaticon.com/authors/freepik
  29. const svg = `<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>`;
  30. const resource = "https://www.savetweetvid.com/result?url=";
  31. let currentUrl = document.location.href;
  32. let updating = false;
  33.  
  34. init(10);
  35.  
  36. locationChange();
  37.  
  38. window.addEventListener("scroll", update);
  39.  
  40. function init(times) {
  41. for (let i = 0; i < times; i++) {
  42. setTimeout(findVideo1, 500 * i);
  43. setTimeout(findVideo2, 500 * i);
  44. setTimeout(sensitiveContent, 500 * i);
  45. }
  46. }
  47.  
  48. function findVideo1() {
  49. // video play button
  50. document.querySelectorAll("[data-testid='playButton']").forEach(button => {
  51. // thumbnail
  52. button.parentElement.querySelectorAll("img:not(.download-set)").forEach(thumbnail => {
  53. thumbnail.classList.add("download-set");
  54. const url = thumbnail.src;
  55. situation(url, thumbnail);
  56. });
  57. });
  58. }
  59.  
  60. function findVideo2() {
  61. // video
  62. document.querySelectorAll("video:not(.download-set)").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 (url.includes("ext_tw_") || url.includes("amplify_") || url.includes("media")) findMenu(video, "video");
  74. // situation 3: unknown
  75. else console.log("Error: Unknown");
  76. }
  77.  
  78. function findMenu(child, type) {
  79. const article = child.closest("article:not(.article-set)");
  80. if (!article) return;
  81. article.classList.add("article-set");
  82. const menus = article.querySelectorAll("[data-testid='caret']");
  83. menus.forEach(menu => menu.addEventListener("click", () => {
  84. clickMenu(article, type, false);
  85. if (type === "gif") clickMenu(article, type, true);
  86. }));
  87. }
  88.  
  89. function clickMenu(article, type, convert) {
  90. // check exist.
  91. if (!!document.querySelector(`.option-download-${convert}-set`)) return;
  92. // wait menu.
  93. if (!document.querySelector("[role='menuitem']")) {
  94. setTimeout(() => clickMenu(article, type, convert), 100);
  95. return;
  96. }
  97. const menu = document.querySelector("[role='menuitem']").parentElement;
  98. // add "download" option.
  99. const option = document.createElement("div");
  100. option.className = "css-1dbjc4n r-1loqt21 r-18u37iz r-1ny4l3l r-ymttw5 r-1yzf0co r-o7ynqc r-6416eg r-13qz1uu option-download-set";
  101. option.addEventListener("mouseenter", () => option.classList.add(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"])));
  102. option.addEventListener("mouseleave", () => option.classList.remove(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"])));
  103. option.addEventListener("click", () => clickDownload(article, type, convert));
  104. // icon
  105. const icon = document.createElement("div");
  106. icon.className = "css-1dbjc4n r-1777fci";
  107. icon.innerHTML = svg;
  108. const svgElement = icon.querySelector("svg");
  109. svgElement.setAttribute("class", "r-4qtqp9 r-yyyyoo r-1q142lx r-1xvli5t r-zso239 r-dnmrzs r-bnwqim r-1plcrui r-lrvibr");
  110. svgElement.classList.add(getTheme(["r-1re7ezh", "r-9ilb82", "r-111h2gw"]));
  111. // text
  112. const text1 = document.createElement("div");
  113. text1.className = "css-1dbjc4n r-16y2uox r-1wbh5a2";
  114. const text2 = document.createElement("div");
  115. text2.className = "css-901oao r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0";
  116. text2.classList.add(getTheme(["r-hkyrab", "r-1fmj7o5", "r-jwli3a"]));
  117. const text3 = document.createElement("span");
  118. text3.className = "css-901oao css-16my406 r-1qd0xha r-ad9z0x r-bcqeeo r-qvutc0";
  119. text3.innerText = getLocalization(type, convert);
  120. // append
  121. menu.appendChild(option);
  122. option.appendChild(icon);
  123. option.appendChild(text1);
  124. text1.appendChild(text2);
  125. text2.appendChild(text3);
  126. }
  127.  
  128. function clickDownload(article, type, convert) {
  129. // gif
  130. if (type === "gif" && !convert) {
  131. let link;
  132. // condition 1: not play yet.
  133. article.querySelectorAll("video").forEach(video => {
  134. link = video.src;
  135. });
  136. // condition 2: playing.
  137. if (!link) {
  138. const image = [...article.querySelectorAll("img")].find(image => image.src.includes("video"));
  139. const id = image.src.split(/[/?]/)[4];
  140. link = `https://video.twimg.com/tweet_video/${id}.mp4`;
  141. }
  142. // open
  143. window.open(link);
  144. }
  145. // video
  146. else {
  147. const title = article.querySelector("time");
  148. const url = !!title ? title.parentElement.href : window.location.href;
  149. window.open(`${resource}${url}`);
  150. }
  151. }
  152.  
  153. function getTheme(array) {
  154. const body = document.querySelector("body");
  155. const color = body.style.backgroundColor; // "rgb(21, 32, 43)"
  156. const red = color.match(/\d+/)[0]; // "21"
  157. switch (red) {
  158. case "255":
  159. return array[0]; // white
  160. case "0":
  161. return array[1]; // black
  162. default:
  163. return array[2]; // gray
  164. }
  165. }
  166.  
  167. function getLocalization(type, convert) {
  168. let download = "Download";
  169. switch (document.querySelector("html").lang) {
  170. case "zh-Hant":
  171. download = "下載";
  172. break;
  173. case "zh":
  174. download = "下载";
  175. break;
  176. case "ja":
  177. download = "ダウンロード";
  178. break;
  179. case "ko":
  180. download = "다운로드";
  181. break;
  182. case "ru":
  183. download = "Скачать";
  184. break;
  185. }
  186.  
  187. let extension = "";
  188. if (type === "gif") extension = convert ? " GIF" : " MP4";
  189.  
  190. return `${download}${extension}`;
  191. }
  192.  
  193. function sensitiveContent() {
  194. // click "view" button on sensitive content warning to run this script again.
  195. document.querySelectorAll(".r-42olwf.r-1vsu8ta:not(.view-set)").forEach(view => {
  196. view.classList.add("view-set");
  197. view.addEventListener("click", () => init(3));
  198. });
  199. }
  200.  
  201. function update() {
  202. if (updating) return;
  203. updating = true;
  204. init(3);
  205. setTimeout(() => { updating = false; }, 1000);
  206. }
  207.  
  208. function locationChange() {
  209. const observer = new MutationObserver(mutations => {
  210. mutations.forEach(() => {
  211. if (currentUrl !== document.location.href) {
  212. currentUrl = document.location.href;
  213. init(10);
  214. }
  215. });
  216. });
  217. const target = document.body;
  218. const config = { childList: true, subtree: true };
  219. observer.observe(target, config);
  220. }
  221.  
  222. })();

QingJ © 2025

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