Threads.net Media Downloader

Add download button for posts with images/videos on Threads.net

当前为 2025-02-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Threads.net Media Downloader
  3. // @namespace https://www.youtube.com/channel/UC26YHf9ASpeu68az2xRKn1w
  4. // @version 03-02-2025
  5. // @description Add download button for posts with images/videos on Threads.net
  6. // @author Kinnena
  7. // @match https://www.threads.net/*
  8. // @icon https://cdn-icons-png.flaticon.com/512/12105/12105338.png
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. function addButtonToElement(element) {
  17. if (element.querySelector('button.my-custom-button')) return;
  18.  
  19. const postContainer = element.closest('div[role="article"]') ||
  20. element.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
  21.  
  22. if (!postContainer) return;
  23.  
  24. const hasMedia = postContainer.querySelector('picture img, video');
  25. if (!hasMedia) return;
  26.  
  27. const button = document.createElement('button');
  28. button.textContent = 'Download';
  29. button.className = 'my-custom-button';
  30. Object.assign(button.style, {
  31. position: 'relative',
  32. background: '#0095f6',
  33. color: 'white',
  34. border: 'none',
  35. borderRadius: '4px',
  36. padding: '6px 12px',
  37. margin: '4px',
  38. cursor: 'pointer'
  39. });
  40.  
  41. button.addEventListener('click', function(event) {
  42. event.preventDefault();
  43. event.stopPropagation();
  44.  
  45. // Get post metadata
  46. const spanElement = postContainer.querySelector('span[class*="x1s688f"]');
  47. const timeElement = postContainer.querySelector('time');
  48. const spanText = (spanElement?.textContent || 'unknown').replace(/[^\w]/g, '_').substring(0, 30);
  49. const datetime = timeElement?.getAttribute('datetime');
  50.  
  51. // Format timestamp
  52. let formattedTime = '';
  53. if (datetime) {
  54. const date = new Date(datetime);
  55. formattedTime = [
  56. date.getFullYear(),
  57. String(date.getMonth() + 1).padStart(2, '0'),
  58. String(date.getDate()).padStart(2, '0'),
  59. '_',
  60. String(date.getHours()).padStart(2, '0'),
  61. String(date.getMinutes()).padStart(2, '0'),
  62. String(date.getSeconds()).padStart(2, '0')
  63. ].join('');
  64. }
  65.  
  66. // Collect media
  67. const mediaElements = [
  68. ...postContainer.querySelectorAll('picture img'),
  69. ...postContainer.querySelectorAll('video')
  70. ];
  71.  
  72. mediaElements.forEach((media, index) => {
  73. let url, type;
  74. if (media.tagName === 'IMG') {
  75. url = media.src;
  76. type = 'image';
  77. } else {
  78. url = media.src || media.querySelector('source')?.src;
  79. type = 'video';
  80. }
  81.  
  82. if (url) {
  83. const extension = getFileExtension(url) || (type === 'image' ? 'jpg' : 'mp4');
  84. const filename = `Threads_${spanText}_${formattedTime}_${index + 1}.${extension}`;
  85. GM_download({
  86. url: url,
  87. name: filename,
  88. onerror: (e) => console.error('Download error:', e)
  89. });
  90. }
  91. });
  92.  
  93. // Auto-like functionality
  94. const likeButton = postContainer.querySelector('[aria-label="讚"]');
  95. if (likeButton) {
  96. likeButton.click();
  97. }
  98. });
  99.  
  100. element.appendChild(button);
  101. }
  102.  
  103. function getFileExtension(url) {
  104. try {
  105. const cleanUrl = url.split(/[?#]/)[0];
  106. return cleanUrl.split('.').pop().toLowerCase();
  107. } catch {
  108. return null;
  109. }
  110. }
  111.  
  112. function scanForButtons() {
  113. document.querySelectorAll('div[class*="x1fc57z9"]').forEach(addButtonToElement);
  114. }
  115.  
  116. // Initial check
  117. scanForButtons();
  118. // Periodic check for new posts
  119. setInterval(scanForButtons, 1000);
  120. })();