Twitter 下载视频

一键导向下载视频的网页。

目前为 2020-09-28 提交的版本。查看 最新版本

  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.4
  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 1: gif
  56. if (url.includes("tweet_")) findMenu(thumbnail, "gif");
  57. // situation 2: video
  58. else if (url.includes("ext_tw_") || url.includes("amplify_") || url.includes("media")) findMenu(thumbnail, "video");
  59. });
  60. });
  61. }
  62.  
  63. function findVideo2() {
  64. // video
  65. document.querySelectorAll("video:not(.download-set)").forEach(video => {
  66. video.classList.add("download-set");
  67. const url = video.poster;
  68. // situation 1: gif
  69. if (url.includes("tweet_")) findMenu(video, "gif");
  70. // situation 2: video
  71. else if (url.includes("ext_tw_") || url.includes("amplify_") || url.includes("media")) findMenu(video, "video");
  72. });
  73. }
  74.  
  75. function findMenu(child, type) {
  76. const article = child.closest("article:not(.article-set)");
  77. if (!article) return;
  78. article.classList.add("article-set");
  79. const menus = article.querySelectorAll("[data-testid='caret']");
  80. menus.forEach(menu => menu.addEventListener("click", () => clickMenu(article, type)));
  81. }
  82.  
  83. function clickMenu(article, type) {
  84. // check exist.
  85. if (!!document.querySelector(".option-download-set")) return;
  86. // wait menu.
  87. if (!document.querySelector("[role='menuitem']")) {
  88. setTimeout(() => clickMenu(article, type), 100);
  89. return;
  90. }
  91. const menu = document.querySelector("[role='menuitem']").parentElement;
  92. // add "download" option.
  93. const option = document.createElement("div");
  94. option.className = "css-1dbjc4n r-1loqt21 r-18u37iz r-1ny4l3l r-1j3t67a r-9qu9m4 r-o7ynqc r-6416eg r-13qz1uu option-download-set";
  95. option.addEventListener("mouseenter", () => option.classList.add(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"])));
  96. option.addEventListener("mouseleave", () => option.classList.remove(getTheme(["r-1u4rsef", "r-1ysxnx4", "r-1uaug3w"])));
  97. option.addEventListener("click", () => clickDownload(article, type));
  98. // icon
  99. const icon = document.createElement("div");
  100. icon.className = "css-1dbjc4n r-1777fci";
  101. icon.innerHTML = svg;
  102. const svgElement = icon.querySelector("svg");
  103. svgElement.setAttribute("class", "r-4qtqp9 r-yyyyoo r-1q142lx r-1xvli5t r-zso239 r-dnmrzs r-bnwqim r-1plcrui r-lrvibr");
  104. svgElement.classList.add(getTheme(["r-1re7ezh", "r-9ilb82", "r-111h2gw"]));
  105. // text
  106. const text1 = document.createElement("div");
  107. text1.className = "css-1dbjc4n r-16y2uox r-1wbh5a2";
  108. const text2 = document.createElement("div");
  109. text2.className = "css-901oao r-1qd0xha r-a023e6 r-16dba41 r-ad9z0x r-bcqeeo r-qvutc0";
  110. text2.classList.add(getTheme(["r-hkyrab", "r-1fmj7o5", "r-jwli3a"]));
  111. const text3 = document.createElement("span");
  112. text3.className = "css-901oao css-16my406 r-1qd0xha r-ad9z0x r-bcqeeo r-qvutc0";
  113. text3.innerText = getLocalization();
  114. // append
  115. menu.appendChild(option);
  116. option.appendChild(icon);
  117. option.appendChild(text1);
  118. text1.appendChild(text2);
  119. text2.appendChild(text3);
  120. }
  121.  
  122. function clickDownload(article, type) {
  123. // gif
  124. if (type === "gif") {
  125. let link;
  126. // condition 1: not play yet.
  127. article.querySelectorAll("video").forEach(video => {
  128. link = video.src;
  129. });
  130. // condition 2: playing.
  131. if (!link) {
  132. const image = [...article.querySelectorAll("img")].find(image => image.src.includes("video"));
  133. const id = image.src.split(/[/?]/)[4];
  134. link = `https://video.twimg.com/tweet_video/${id}.mp4`;
  135. }
  136. // open
  137. window.open(link);
  138. }
  139. // video
  140. else {
  141. const title = article.querySelector("time");
  142. const url = !!title ? title.parentElement.href : window.location.href;
  143. window.open(`${resource}${url}`);
  144. }
  145. }
  146.  
  147. function getTheme(array) {
  148. const body = document.querySelector("body");
  149. const color = body.style.backgroundColor; // "rgb(21, 32, 43)"
  150. const red = color.match(/\d+/)[0]; // "21"
  151. switch (red) {
  152. case "255":
  153. return array[0]; // white
  154. case "0":
  155. return array[1]; // black
  156. default:
  157. return array[2]; // gray
  158. }
  159. }
  160.  
  161. function getLocalization() {
  162. switch (document.querySelector("html").lang) {
  163. case "zh-Hant":
  164. return "下載";
  165. case "zh":
  166. return "下载";
  167. case "ja":
  168. return "ダウンロード";
  169. case "ko":
  170. return "다운로드";
  171. case "ru":
  172. return "Скачать вниз";
  173. default:
  174. return "Download";
  175. }
  176. }
  177.  
  178. function sensitiveContent() {
  179. // click "view" button on sensitive content warning to run this script again.
  180. document.querySelectorAll(".r-42olwf.r-1vsu8ta:not(.view-set)").forEach(view => {
  181. view.classList.add("view-set");
  182. view.addEventListener("click", () => init(3));
  183. });
  184. }
  185.  
  186. function update() {
  187. if (updating) return;
  188. updating = true;
  189. init(3);
  190. setTimeout(() => { updating = false; }, 1000);
  191. }
  192.  
  193. function locationChange() {
  194. const observer = new MutationObserver(mutations => {
  195. mutations.forEach(() => {
  196. if (currentUrl !== document.location.href) {
  197. currentUrl = document.location.href;
  198. init(10);
  199. }
  200. });
  201. });
  202. const target = document.body;
  203. const config = { childList: true, subtree: true };
  204. observer.observe(target, config);
  205. }
  206.  
  207. })();

QingJ © 2025

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