Greasy Fork is available in English.

TwitterImg Downloader

Add download button to Twitter image, and click to download the original image named by format.

  1. // ==UserScript==
  2. // @name TwitterImg Downloader
  3. // @namespace TypeNANA
  4. // @version 0.15
  5. // @description Add download button to Twitter image, and click to download the original image named by format.
  6. // @author HY
  7. // @include *://twitter.com/*
  8. // @include *://*.twitter.com/*
  9. // @include *://x.com/*
  10. // @include *://*.x.com/*
  11. // @require http://code.jquery.com/jquery-3.3.1.min.js
  12. // @grant none
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. /** Edit defaultFileName to change the file name format
  18. *
  19. * <%Userid> Twitter user ID. eg: shiratamacaron
  20. * <%Tid> Tweet ID. eg: 1095705491429158912
  21. * <%Time> Current timestamp. eg: 1550557810891
  22. * <%PicName> Original pic name. eg: DzS6RkJUUAA_0LX
  23. * <%PicNo> Ordinal number of pic. eg: 0
  24. *
  25. * default: "<%Userid> <%Tid>_p<%PicNo>"
  26. * result: "shiratamacaron 1095705491429158912_p0.jpg"
  27. *
  28. * example1: "<%Userid> <%Tid> <%PicName>”
  29. * result: "shiratamacaron 1095705491429158912 DzS6RkJUUAA_0LX.jpg"
  30. *
  31. * example2: "<%Tid>_p<%PicNo>”
  32. * result: "1095705491429158912_p0.jpg"
  33. */
  34. let defaultFileName = "<%Userid> <%Tid>_p<%PicNo>";
  35.  
  36.  
  37. /** Edit following value to change download shortcut key in gallery mode
  38. * KeyCode value can be found at https://keycode.info/
  39. *
  40. * default: shift + s (s->83)
  41. */
  42. let shortCut_Shift = true; //true - Yes , false - No
  43. let shortCut_Ctrl = false;
  44. let shortCut_Alt = false;
  45. let shortCut_KeyCode = 83 //KeyCode value
  46.  
  47. function download(url, name, view) {
  48. //通过fetch获取blob
  49. fetch(url).then(response => {
  50. if (response.status == 200)
  51. return response.blob();
  52. throw new Error(`status: ${response.status}.`)
  53. }).then(blob => {
  54. downloadFile(name, blob, view)
  55. }).catch(error => {
  56. console.log("failed. cause:", error)
  57. })
  58. }
  59.  
  60. function downloadFile(fileName, blob, view) {
  61. //通过a标签的download属性来下载指定文件名的文件
  62. let anchor = view;
  63. let src = URL.createObjectURL(blob);
  64. anchor.download = fileName;
  65. anchor.href = src;
  66. view.click();
  67. }
  68.  
  69. const addDownloadButton = function (v) {
  70. if (newVersionFlag) {
  71. newVer(v);
  72. } else {
  73. oldVer(v)
  74. }
  75. return false;
  76. }
  77.  
  78. function addDlBtn(){
  79. let btnDownloadImg;
  80. if(document.getElementsByClassName("img-link").length==0){
  81. btnDownloadImg = document.createElement('A');
  82. btnDownloadImg.className = 'img-link';
  83. document.getElementById("react-root").appendChild(btnDownloadImg);
  84. }else{
  85. btnDownloadImg = document.getElementsByClassName("img-link")[0];
  86. }
  87. return btnDownloadImg;
  88. }
  89.  
  90. function newVer(v) {
  91. if (v == null || v.length == 0) return;
  92. let target = v[0];
  93. if (target == null || target.src == null) return;
  94. if (target.alt == null || target.alt == "") return;
  95. if (target.parentElement.getAttribute("aria-label") == null || target.parentElement.getAttribute("aria-label") == "") return;
  96. let dlbtn = document.createElement('DIV');
  97. target.parentElement.parentElement.appendChild(dlbtn);
  98. dlbtn.outerHTML = '<div class="dl_btn_div" style="cursor: pointer;z-index: 999;display: table;font-size: 15px;color: white;position: absolute;right: 5px;bottom: 5px;background: #0000007f;height: 30px;width: 30px;border-radius: 15px;text-align: center;"><svg class="icon" style="width: 15px;height: 15px;vertical-align: top;display: inline-block;margin-top: 7px;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3658"><path d="M925.248 356.928l-258.176-258.176a64 64 0 0 0-45.248-18.752H144a64 64 0 0 0-64 64v736a64 64 0 0 0 64 64h736a64 64 0 0 0 64-64V402.176a64 64 0 0 0-18.752-45.248zM288 144h192V256H288V144z m448 736H288V736h448v144z m144 0H800V704a32 32 0 0 0-32-32H256a32 32 0 0 0-32 32v176H144v-736H224V288a32 32 0 0 0 32 32h256a32 32 0 0 0 32-32V144h77.824l258.176 258.176V880z" p-id="3659"></path></svg></div>';
  99. dlbtn = target.parentElement.parentElement.getElementsByClassName("dl_btn_div")[0];
  100.  
  101. let btnDownloadImg = addDlBtn();
  102. let urlregex = /https\:\/\/(twitter|x).com\//;
  103.  
  104. if (!document.location.href.includes("photo")) {
  105. //信息流模式
  106.  
  107. let firstA = findFirstA(target);
  108. //获取文件名
  109. // https://pbs.twimg.com/media/D_mR-WEUYAAZJVH?format=jpg&amp;name=360x360
  110. let fooName = target.src.split("?")[0];
  111. let barName = fooName.split("/");
  112. let dl_picname = barName[barName.length - 1];
  113. let dl_time = new Date().getTime();
  114.  
  115. //获取图片编号
  116. // ameto_y/status/1151067160078274561/photo/1
  117. let array = firstA.href.replace(urlregex, "").split("/");
  118. let dl_userid = array[0];
  119. let dl_tid = array[2];
  120. let dl_picno = array[4];
  121. //替换内容,拼接文件名
  122. let dl_filename = defaultFileName
  123. .replace("<%Userid>", dl_userid)
  124. .replace("<%Tid>", dl_tid)
  125. .replace("<%Time>", dl_time)
  126. .replace("<%PicName>", dl_picname)
  127. .replace("<%PicNo>", dl_picno - 1);
  128. //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
  129. let dl_ext = "jpg";
  130. if (target.src.includes("format=png")) {
  131. dl_ext = "png";
  132. }
  133. dlbtn.addEventListener('touchstart', function (e) {
  134. dlbtn.onclick = function (e) {
  135. return false;
  136. }
  137. return false;
  138. });
  139. dlbtn.addEventListener('mousedown', function (e) {
  140. dlbtn.onclick = function (e) {
  141. return false;
  142. }
  143. return false;
  144. });
  145. dlbtn.addEventListener('click', function (e) {
  146. //调用下载方法
  147. cancelBubble(e);
  148. download("https://pbs.twimg.com/media/" + dl_picname + "?format=" + dl_ext + "&name=orig", dl_filename + "." + dl_ext, btnDownloadImg);
  149. return false;
  150. });
  151. } else {
  152. //大图画廊模式
  153.  
  154. //获取文件名
  155. // https://pbs.twimg.com/media/D_mR-WEUYAAZJVH?format=jpg&amp;name=360x360
  156. let fooName = target.src.split("?")[0];
  157. let barName = fooName.split("/");
  158. let dl_picname = barName[barName.length - 1];
  159.  
  160. //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
  161. let dl_ext = "jpg";
  162. if (target.src.includes("format=png")) {
  163. dl_ext = "png";
  164. }
  165. dlbtn.addEventListener('click', function (e) {
  166. //调用下载方法
  167. cancelBubble(e);
  168.  
  169. //获取图片编号
  170. // ameto_y/status/1151067160078274561/photo/1
  171. let array = document.location.href.replace(urlregex, "").split("/");
  172. let dl_userid = array[0];
  173. let dl_tid = array[2];
  174. let dl_picno = array[4];
  175. let dl_time = new Date().getTime();
  176. let dl_filename = defaultFileName
  177. .replace("<%Userid>", dl_userid)
  178. .replace("<%Tid>", dl_tid)
  179. .replace("<%Time>", dl_time)
  180. .replace("<%PicName>", dl_picname)
  181. .replace("<%PicNo>", dl_picno - 1);
  182. download("https://pbs.twimg.com/media/" + dl_picname + "?format=" + dl_ext + "&name=orig", dl_filename + "." + dl_ext, btnDownloadImg);
  183. return false;
  184. });
  185. }
  186. }
  187.  
  188. //画廊模式下的快捷键功能
  189. function onShortCut() {
  190. let locationArr = document.location.href.split("/");
  191. let targetArr = $('ul[role="list"]');
  192.  
  193. let imgNo = null;
  194. let imgArr = null;
  195. let target = null;
  196. //判断是否找到了画廊的ul标签
  197. if (targetArr.length == 0 ) {
  198. //如果找不到ul标签,并且不是画廊模式(那么网址内没有“photo”), 则不进行进一步的处理
  199. if(locationArr.length < 2 || locationArr[locationArr.length - 2] != "photo") return;
  200. //否则进行搜索单张图画廊的情况
  201. var arr = $('img[src^="https://pbs.twimg.com/media/');
  202. for(var i=0;i< arr.length;i++){
  203. var imgUrl = arr[i].src.split("?")[i]
  204. //判断是否是推特附带的图片img,并且判断是不是信息流的图片(信息流图片的所有母层中必定带有一个a标签)
  205. if(arr[i].parentElement.firstElementChild!="" && arr[i].parentElement.firstElementChild.style.backgroundImage.includes(imgUrl) && findFirstA(arr[i])==null){
  206. //单图模式的赋值
  207. let imgNo = 0;
  208. target = arr[i];
  209. break;
  210. }
  211. }
  212. //如果找不到任何目标,则不进行进一步的处理
  213. if(target == null) return;
  214. } else {
  215. //多图模式的赋值
  216. imgNo = locationArr[locationArr.length - 1] - 1;
  217. imgArr = targetArr[0].getElementsByTagName("img");
  218. target = imgArr[imgNo];
  219. }
  220.  
  221. //获取文件名
  222. // https://pbs.twimg.com/media/D_mR-WEUYAAZJVH?format=jpg&amp;name=360x360
  223. let fooName = target.src.split("?")[0];
  224. let barName = fooName.split("/");
  225. let dl_picname = barName[barName.length - 1];
  226.  
  227. //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
  228. let dl_ext = "jpg";
  229. if (target.src.includes("format=png")) {
  230. dl_ext = "png";
  231. }
  232. //获取图片编号
  233. // ameto_y/status/1151067160078274561/photo/1
  234. let urlregex = /https\:\/\/(twitter|x).com\//;
  235. let array = document.location.href.replace(urlregex, "").split("/");
  236. let dl_userid = array[0];
  237. let dl_tid = array[2];
  238. let dl_picno = array[4];
  239. let dl_time = new Date().getTime();
  240. let dl_filename = defaultFileName
  241. .replace("<%Userid>", dl_userid)
  242. .replace("<%Tid>", dl_tid)
  243. .replace("<%Time>", dl_time)
  244. .replace("<%PicName>", dl_picname)
  245. .replace("<%PicNo>", dl_picno - 1);
  246.  
  247. let btnDownloadImg = addDlBtn();
  248. download("https://pbs.twimg.com/media/" + dl_picname + "?format=" + dl_ext + "&name=orig", dl_filename + "." + dl_ext, btnDownloadImg);
  249. }
  250.  
  251. function oldVer(v) {
  252. let tweets = document.querySelectorAll('.tweet');
  253. tweets.forEach((t) => {
  254. //忽略视频信息
  255. if (t.getElementsByClassName("PlayableMedia").length > 0) return;
  256. //文件名信息
  257. let dl_userid = t.getAttribute("data-screen-name");
  258. let dl_name = t.getAttribute("data-name");
  259. let dl_tid = t.getAttribute("data-tweet-id");
  260. //尝试获取发推时间,但是部分情况无法获取,故采用保存文件时间
  261. //let dl_time = t.getElementsByClassName("_timestamp")[0].getAttribute("data-time");
  262. let dl_time = new Date().getTime();
  263. /* 画廊 */
  264. if (t.parentElement.className.includes("GalleryTweet")) {
  265. //获取画廊容器
  266. let imgContent = t.parentElement.parentElement.getElementsByClassName("Gallery-media")[0];
  267. //防止按钮重复叠加
  268. if (imgContent.parentElement.parentElement.getElementsByClassName("dl_btn_div").length != 0) return;
  269. //创建下载按钮
  270. let dlbtn = document.createElement('div');
  271. imgContent.parentElement.appendChild(dlbtn);
  272. dlbtn.outerHTML = '<div class="dl_btn_div" style="z-index: 999;display: table;font-size: 15px;color: white;position: absolute;right: 5px;top: 5px;background: #0000007f;height: 30px;width: 30px;border-radius: 15px;text-align: center;"><a style="display: table-cell;height: 30px;width: 30px;vertical-align: middle;color:white;font-family:edgeicons;text-decoration: none;user-select: none;" id="a_dl">&#xf088</a></div>';
  273. dlbtn = imgContent.parentElement.getElementsByClassName("dl_btn_div")[0];
  274. //创建不可见的下载用标签
  275. let btnDownloadImg = document.createElement('A');
  276. btnDownloadImg.className = 'img-link';
  277. imgContent.parentElement.parentElement.appendChild(btnDownloadImg);
  278. //添加点击事件
  279. dlbtn.addEventListener('click', function () {
  280. //去掉图片链接尾部的 ":large"
  281. let ImgUrl = imgContent.getElementsByClassName("media-image")[0].src.replace(":large", "");
  282. //获取文件名
  283. let dl_picname = ImgUrl.replace('https://pbs.twimg.com/media/', '').replace('.png', '').replace('.jpg', '');
  284. //设置默认图片编号0
  285. let dl_picno = 0;
  286. //个人页面class
  287. let Images = imgContent.parentElement.querySelectorAll('.AdaptiveMedia-container img');
  288. if (Images.length <= 0) {
  289. //信息流class
  290. Images = imgContent.parentElement.querySelectorAll('.AdaptiveMedia-photoContainer img');
  291. }
  292. //通过循环比较获取图片序号
  293. for (var imgNo = 0; imgNo < Images.length; imgNo++) {
  294. if (ImgUrl == Images[imgNo].src) {
  295. dl_picno = imgNo;
  296. break;
  297. }
  298. }
  299. //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
  300. let dl_ext = ".jpg";
  301. if (ImgUrl.includes(".png")) {
  302. dl_ext = ".png";
  303. }
  304. //替换内容,拼接文件名
  305. let dl_filename = defaultFileName
  306. .replace("<%Userid>", dl_userid)
  307. .replace("<%Name>", dl_name)
  308. .replace("<%Tid>", dl_tid)
  309. .replace("<%Time>", dl_time)
  310. .replace("<%PicName>", dl_picname)
  311. .replace("<%PicNo>", dl_picno);
  312. //调用下载方法
  313. download(ImgUrl + ":orig", dl_filename + dl_ext, btnDownloadImg);
  314. });
  315. return;
  316. }
  317. /* 信息流 */
  318. //防止按钮重复叠加
  319. if (t.getElementsByClassName("dl_btn_div").length != 0) return;
  320. //获取全部图片标签
  321. let Images = t.querySelectorAll('.AdaptiveMedia-container img');
  322. for (var i = 0; i < Images.length; i++) {
  323. let Img = Images[i];
  324. if (Img) {
  325. //获取图片链接
  326. let ImgUrl = Img.src;
  327. //如果为blob对象则跳过
  328. if (Img.src.includes('blob')) break;
  329. //创建下载按钮
  330. let dlbtn = document.createElement('div');
  331. Img.parentElement.parentElement.appendChild(dlbtn);
  332. dlbtn.outerHTML = '<div class="dl_btn_div" style="display: table;font-size: 15px;color: white;position: absolute;right: 5px;bottom: 5px;background: #0000007f;height: 30px;width: 30px;border-radius: 15px;text-align: center;"><a style="display: table-cell;height: 30px;width: 30px;vertical-align: middle;color:white;font-family:edgeicons;text-decoration: none;user-select: none;" id="a_dl">&#xf088</a></div>';
  333. dlbtn = Img.parentElement.parentElement.getElementsByClassName("dl_btn_div")[0];
  334. //创建不可见的下载用标签
  335. let btnDownloadImg = document.createElement('A');
  336. btnDownloadImg.className = 'img-link';
  337. t.appendChild(btnDownloadImg);
  338. //获取文件名
  339. let dl_picname = Img.src.replace('https://pbs.twimg.com/media/', '').replace('.png', '').replace('.jpg', '');
  340. //获取图片编号
  341. let dl_picno = i;
  342. //替换内容,拼接文件名
  343. let dl_filename = defaultFileName
  344. .replace("<%Userid>", dl_userid)
  345. .replace("<%Name>", dl_name)
  346. .replace("<%Tid>", dl_tid)
  347. .replace("<%Time>", dl_time)
  348. .replace("<%PicName>", dl_picname)
  349. .replace("<%PicNo>", dl_picno);
  350. //获取拓展名,推特只存在.jpg和.png格式的图片,故偷个懒不做正则判断
  351. let dl_ext = ".jpg";
  352. if (ImgUrl.includes(".png")) {
  353. dl_ext = ".png";
  354. }
  355. //添加点击事件
  356. dlbtn.addEventListener('click', function () {
  357. //调用下载方法
  358. download(ImgUrl + ":orig", dl_filename + dl_ext, btnDownloadImg);
  359. });
  360. }
  361. };
  362. });
  363. }
  364.  
  365. let newVersionFlag = (document.getElementById("react-root") != null);
  366. if (newVersionFlag) {
  367. waitForKeyElements(
  368. 'img[src^="https://pbs.twimg.com/media/"]',
  369. addDownloadButton
  370. );
  371. } else {
  372. waitForKeyElements(
  373. '.AdaptiveMedia-container img',
  374. addDownloadButton
  375. );
  376. }
  377. $(document).keyup(function (e) {
  378. let shiftFlag = true
  379. let ctrlFlag = true
  380. let altFlag = true
  381. if (shortCut_Shift) {
  382. shiftFlag = e.shiftKey
  383. }
  384. if (shortCut_Ctrl) {
  385. ctrlFlag = e.ctrlKey
  386. }
  387. if (shortCut_Alt) {
  388. altFlag = e.altKey
  389. }
  390. if (e.keyCode == shortCut_KeyCode && shiftFlag && ctrlFlag && altFlag) {
  391. onShortCut()
  392. }
  393. })
  394.  
  395. function waitForKeyElements(
  396. selectorTxt,
  397. actionFunction,
  398. bWaitOnce,
  399. iframeSelector
  400. ) {
  401. var targetNodes, btargetsFound;
  402.  
  403. if (typeof iframeSelector == "undefined") {
  404. targetNodes = $(selectorTxt);
  405. } else {
  406. targetNodes = $(iframeSelector).contents().find(selectorTxt);
  407. }
  408.  
  409. if (targetNodes && targetNodes.length > 0) {
  410. btargetsFound = true;
  411. targetNodes.each(function () {
  412. var jThis = $(this);
  413. var alreadyFound = jThis.data('alreadyFound') || false;
  414.  
  415. if (!alreadyFound) {
  416. var cancelFound = actionFunction(jThis);
  417. if (cancelFound) {
  418. btargetsFound = false;
  419. } else {
  420. jThis.data('alreadyFound', true);
  421. }
  422. }
  423. });
  424. } else {
  425. btargetsFound = false;
  426. }
  427.  
  428. var controlObj = waitForKeyElements.controlObj || {};
  429. var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  430. var timeControl = controlObj[controlKey];
  431.  
  432. if (btargetsFound && bWaitOnce && timeControl) {
  433. clearInterval(timeControl);
  434. delete controlObj[controlKey]
  435. } else {
  436. if (!timeControl) {
  437. timeControl = setInterval(function () {
  438. waitForKeyElements(selectorTxt,
  439. actionFunction,
  440. bWaitOnce,
  441. iframeSelector
  442. );
  443. }, 300);
  444. controlObj[controlKey] = timeControl;
  445. }
  446. }
  447. waitForKeyElements.controlObj = controlObj;
  448. }
  449.  
  450. function findFirstA(node) {
  451. var tmp = node;
  452. for (var i = 0; i < 20; i++) {
  453. tmp = tmp.parentElement
  454. if (tmp == null) return null;
  455. if (tmp.nodeName == "a" || tmp.nodeName == "A") {
  456. return tmp
  457. }
  458. }
  459. }
  460. function findFirstLi(node) {
  461. var tmp = node;
  462. for (var i = 0; i < 20; i++) {
  463. tmp = tmp.parentElement
  464. if (tmp == null) return null;
  465. if (tmp.nodeName == "li" || tmp.nodeName == "LI") {
  466. return tmp
  467. }
  468. }
  469. }
  470. function cancelBubble(e) {
  471. var evt = e ? e : window.event;
  472. if (evt.stopPropagation) {
  473. evt.stopPropagation();
  474. } else {
  475. evt.cancelBubble = true;
  476. }
  477. }
  478. })();