Twitter Image Download Mod

Download images in tweets

  1. // ==UserScript==
  2. // @name Twitter Image Download Mod
  3. // @namespace xuyiming.open@outlook.com
  4. // @description Download images in tweets
  5. // @author xymopen
  6. // @author pks
  7. // @version 1.0.3
  8. // @run-at document-end
  9. // @include https://twitter.com*
  10. // @match https://twitter.com*
  11. // @license BSD 2-Clause
  12. // @grant GM_addStyle
  13. // @grant GM_xmlhttpRequest
  14. // @connect pbs.twimg.com
  15. // ==/UserScript==
  16.  
  17. ( function () {
  18. "use strict";
  19.  
  20. const Fragments = {
  21. fromString: domstring => {
  22. let divEl = document.createElement( "div" );
  23.  
  24. divEl.innerHTML = domstring;
  25.  
  26. return Array.from( divEl.childNodes ).reduce( ( fragment, node ) => {
  27. fragment.appendChild( node );
  28.  
  29. return fragment;
  30. }, document.createDocumentFragment() );
  31. },
  32.  
  33. fromIterable: nodeList =>
  34. Array.from( nodeList ).reduce( ( fragment, node ) => {
  35. fragment.appendChild( node );
  36.  
  37. return fragment;
  38. }, document.createDocumentFragment() )
  39. };
  40.  
  41. function stringToFragment( domstring ) {
  42. let divEl = document.createElement( "div" );
  43.  
  44. divEl.innerHTML = domstring;
  45.  
  46. return Array.from( divEl.childNodes ).reduce( ( fragment, node ) => {
  47. fragment.appendChild( node );
  48.  
  49. return fragment;
  50. }, document.createDocumentFragment() );
  51. }
  52.  
  53. function xGM_bufferRequest( request ) {
  54. // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Handling_binary_data
  55. let onload = request.onload;
  56.  
  57. return GM_xmlhttpRequest( Object.assign( request, {
  58. overrideMimeType: "text/plain; charset=x-user-defined",
  59. onload: ( response ) => {
  60. let buffer = new Uint8Array( response.responseText.length );
  61.  
  62. for ( let i = 0; i < response.responseText.length; i += 1 ) {
  63. buffer[ i ] = response.responseText.charCodeAt( i );
  64. }
  65.  
  66. onload.call( request, buffer );
  67. }
  68. } ) );
  69. }
  70.  
  71. let theDom = Fragments.fromString(
  72. '<div class="_download">' +
  73. '<a>' +
  74. '<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25">' +
  75. // steal from GitHub's Octicons licensed under
  76. // MIT(https://github.com/primer/octicons/blob/master/LICENSE)
  77. '<path ' +
  78. 'd="' +
  79. 'M4 ' +
  80. '6h3V0h2v6h3l-4 ' +
  81. '4-4-4zm11-4h-4v1h4v8H1V3h4V2H1c-.55 ' +
  82. '0-1 ' +
  83. '.45-1 ' +
  84. '1v9c0 ' +
  85. '.55.45 ' +
  86. '1 ' +
  87. '1 ' +
  88. '1h5.34c-.25.61-.86 ' +
  89. '1.39-2.34 ' +
  90. '2h8c-1.48-.61-2.09-1.39-2.34-2H15c.55 ' +
  91. '0 ' +
  92. '1-.45 ' +
  93. '1-1V3c0-.55-.45-1-1-1z"' +
  94. 'transform="scale(1.56)"' +
  95. 'fill="rgba(255, 255, 255, 0.4)" />' +
  96. '</svg>' +
  97. '</a>' +
  98. '</div>'
  99. );
  100.  
  101. GM_addStyle(
  102. '._download {' +
  103. 'border-radius: 5px;' +
  104. 'width: 35px;' +
  105. 'height: 35px;' +
  106. 'background-color: rgba(105, 105, 105, 0.2);' +
  107. 'position: absolute;' +
  108. 'right: 5px;' +
  109. 'bottom: 5px;' +
  110. '}' +
  111.  
  112. '._download:hover {' +
  113. 'background-color: rgba(105, 105, 105, 0.4);' +
  114. '}' +
  115.  
  116. '._download svg {' +
  117. 'position: relative;' +
  118. 'top: 5px;' +
  119. 'left: 5px;"' +
  120. '}'
  121. );
  122.  
  123. function onInsert( doc ) {
  124. let photoContainers = doc.querySelectorAll( ".permalink-tweet .AdaptiveMedia-photoContainer" );
  125.  
  126. photoContainers.forEach( ( photoContainer, index ) => {
  127. let dom = theDom.cloneNode( true ),
  128. a = dom.querySelector( "a" );
  129.  
  130. a.href = "#";
  131. a.dataset.state = "unfetched";
  132.  
  133. // according to http://www.zhangxinxu.com/wordpress/2016/04/know-about-html-download-attribute/
  134. // cannot rename an crossing-domain resource
  135. a.addEventListener( "click", function ( event ) {
  136. event.stopPropagation();
  137. event.stopImmediatePropagation();
  138.  
  139. if ( "unfetched" === a.dataset.state ) {
  140. let img = photoContainer.querySelector( "img" ),
  141. basename = location.pathname.replace(
  142. /\/(.+)\/status\/(.+)/,
  143. ( $0, user, tweet ) => `twitter_${ user }_${ tweet }_${ index }`
  144. ),
  145. extname = img.src.match( /(?=.)\w+$/ ).toString(),
  146. mime = ( {
  147. "jpg": "image/jpeg",
  148. "jepg": "image/jpeg",
  149. "png": "image/png",
  150. "gif": "image/gif",
  151. } )[ extname ] || "application/octet-stream";
  152.  
  153. event.preventDefault();
  154.  
  155. a.dataset.state = "fetching";
  156.  
  157. xGM_bufferRequest( {
  158. method: "GET",
  159. url: img.src+':orig',
  160. headers: { referer: document.URL },
  161. onload: buffer => {
  162. console.log(img.src);
  163. if ( "fetching" === a.dataset.state ) {
  164. let blob = new Blob( [ buffer ], { "type": mime } ),
  165. blobURL = URL.createObjectURL( blob );
  166.  
  167. a.href = blobURL;
  168. a.download = `${ basename }.${ extname }`;
  169. a.dataset.state = "fetched";
  170. a.click();
  171. }
  172. },
  173. onerror: () => {
  174. if ( "fetching" === a.dataset.state ) {
  175. a.dataset.state = "error";
  176. alert( `Fail to fetch image ${ img.src }` );
  177. }
  178. }
  179. } );
  180. }
  181. }, true );
  182.  
  183. photoContainer.appendChild( dom );
  184. } );
  185. }
  186.  
  187. function onRemove( doc ) {
  188. Array.from( doc.querySelectorAll( "._download a" ) ).forEach( a => {
  189. if ( "fetched" === a.dataset.state ) {
  190. URL.revokeObjectURL( a.href );
  191. }
  192.  
  193. a.dataset.state = "removed";
  194. } );
  195. }
  196.  
  197. let target = document.querySelector( ".PermalinkOverlay-body" );
  198. let mutationConfig = {childList: true};
  199. let mutation = new MutationObserver( mutations => {
  200. mutations.forEach( mutation => {
  201. if ( mutation.removedNodes.length > 0 ) {
  202. onRemove( Fragments.fromIterable( mutation.removedNodes ) );
  203. }
  204.  
  205. if ( mutation.addedNodes.length > 0 ) {
  206. onInsert( target );
  207. }
  208. } );
  209. } );
  210. mutation.observe(target, mutationConfig);
  211.  
  212. if ( target.children.length > 0 ) {
  213. onInsert( target );
  214. }
  215. } )();

QingJ © 2025

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