Threads Video Downloader

Download photos and videos from Threads quickly and easily!

  1. // ==UserScript==
  2. // @name Threads Video Downloader
  3. // @namespace https://github.com/ManoloZocco/Threads-video-downloader-userscript
  4. // @version 1.3.11
  5. // @description Download photos and videos from Threads quickly and easily!
  6. // @author P0L1T3 aka Manolo Zocco
  7. // @match https://*.threads.net/*
  8. // @connect *
  9. // @grant GM_download
  10. // @grant GM_addStyle
  11. // @grant GM_xmlhttpRequest
  12. // @run-at document-end
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // Inserisce il CSS (prende spunto da interface.css dell'addon Firefox)
  20. GM_addStyle(`
  21. .dw {
  22. position: absolute;
  23. z-index: 5;
  24. width: 116px;
  25. height: 34px;
  26. border-radius: 8px;
  27. background: rgba(0, 0, 0, 0.6);
  28. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  29. display: none;
  30. flex-direction: row;
  31. justify-content: space-around;
  32. align-items: center;
  33. cursor: pointer;
  34. margin: 7px;
  35. border: none;
  36. color: #FFF;
  37. font-size: 11px;
  38. font-weight: 600;
  39. text-transform: uppercase;
  40. line-height: 22px;
  41. letter-spacing: -0.42px;
  42. bottom: 7px;
  43. left: 7px;
  44. }
  45. .dw .icon {
  46. background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 3v9' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3Cpath d='M6 10l4 4 4-4' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Crect x='4' y='14' width='12' height='2' fill='white'/%3E%3C/svg%3E");
  47. width: 20px;
  48. height: 20px;
  49. margin-right: 5px;
  50. }
  51. *:hover > .dw {
  52. display: flex;
  53. }
  54. .dw:hover {
  55. background: rgba(0, 0, 0, 0.8);
  56. }
  57. `);
  58.  
  59. // Funzione per il download: utilizza GM_download (con fallback se necessario)
  60. function downloadFile(url) {
  61. if (!url) return;
  62. let fileName = url.substring(url.lastIndexOf('/') + 1) || 'download';
  63. GM_download({
  64. url: url,
  65. name: fileName,
  66. onerror: function(err) {
  67. console.error('GM_download error:', err);
  68. fallbackDownload(url, fileName);
  69. }
  70. });
  71. }
  72.  
  73. function fallbackDownload(url, fileName) {
  74. GM_xmlhttpRequest({
  75. method: "GET",
  76. url: url,
  77. responseType: "blob",
  78. onload: function(response) {
  79. const blob = response.response;
  80. const blobUrl = URL.createObjectURL(blob);
  81. const a = document.createElement("a");
  82. a.href = blobUrl;
  83. a.download = fileName;
  84. document.body.appendChild(a);
  85. a.click();
  86. document.body.removeChild(a);
  87. URL.revokeObjectURL(blobUrl);
  88. },
  89. onerror: function(error) {
  90. console.error('Fallback download error:', error);
  91. }
  92. });
  93. }
  94.  
  95. // Oggetto per iniettare il pulsante come fa l'addon Firefox (injector.js)
  96. const downloader = {
  97. observeDom() {
  98. // Per i video: usa semplicemente l'attributo "src" come fa l'addon Firefox
  99. document.querySelectorAll("video").forEach((video) => {
  100. let container = this.findRoot(video);
  101. if (!container) return;
  102. if (container.querySelector(".dw")) return;
  103. let url = video.getAttribute("src") || null;
  104. // Aggiungi il pulsante solo se l'URL contiene ".mp4"
  105. if (url && url.toLowerCase().indexOf(".mp4") !== -1) {
  106. container.appendChild(this.getBtn(url));
  107. }
  108. });
  109. // Per le immagini: usa "src" e, se l'immagine è grande, la inserisce
  110. document.querySelectorAll("img").forEach((img) => {
  111. if (img.width < 200 || img.height < 200) return;
  112. if (img.parentElement.querySelector(".dw")) return;
  113. let url = img.getAttribute("src") || null;
  114. if (url && (url.toLowerCase().endsWith(".jpg") || url.toLowerCase().endsWith(".jpeg") ||
  115. url.toLowerCase().endsWith(".png") || url.toLowerCase().endsWith(".gif"))) {
  116. img.parentElement.prepend(this.getBtn(url));
  117. }
  118. });
  119. },
  120. // Crea il pulsante di download; imita getBtn dell'addon Firefox usando browser.i18n.getMessage("btn_title")
  121. getBtn(url) {
  122. let btn = document.createElement("button");
  123. btn.innerText = "Download";
  124. btn.className = "dw";
  125. let icon = document.createElement("span");
  126. icon.className = "icon";
  127. btn.appendChild(icon);
  128. btn.setAttribute("src", url);
  129. btn.addEventListener("click", this.dw);
  130. return btn;
  131. },
  132. // Cerca ricorsivamente un container adatto (come fa findRoot nell'addon Firefox)
  133. findRoot(el) {
  134. let parent = el.parentNode;
  135. if (!parent) return null;
  136. let candidate = parent.querySelector("div[data-visualcompletion]");
  137. return candidate || this.findRoot(parent);
  138. },
  139. // Handler del click: esegue il download inviando il "src" come fa l'addon Firefox
  140. dw(event) {
  141. event.preventDefault();
  142. event.stopPropagation();
  143. let btn = (event.target.nodeName.toLowerCase() === "button") ? event.target : event.target.parentElement;
  144. let url = btn.hasAttribute("src") ? btn.getAttribute("src") : null;
  145. if (url) {
  146. downloadFile(url);
  147. }
  148. }
  149. };
  150.  
  151. // Inietta il pulsante a intervalli (simile al setInterval in injector.js)
  152. function init() {
  153. downloader.observeDom();
  154. }
  155. setInterval(init, 500);
  156. new MutationObserver(init).observe(document.body, { childList: true, subtree: true });
  157. })();