Guest User

attach X video/images

a guest
Mar 6th, 2026
99
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.97 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Better Meguca - X Media Import
  3. // @namespace Anon
  4. // @version 3.0
  5. // @description Import X Videos and Images
  6. // @match https://sturdychan.help/*
  7. // @match https://easychan.net/*
  8. // @match https://mokachan.cafe/*
  9. // @match https://noschizos.club/*
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_addStyle
  12. // @connect api.fxtwitter.com
  13. // @connect video.twimg.com
  14. // @connect pbs.twimg.com
  15. // @run-at document-idle
  16. // ==/UserScript==
  17.  
  18. (async function () {
  19. 'use strict';
  20.  
  21. const MAX_SIZE_MB = 100;
  22.  
  23. GM_addStyle(`
  24. .attach-twitter-form {
  25. display:flex; flex-direction:column; gap:8px; margin-top:12px;
  26. background: #000 !important; padding:12px; border-radius:4px;
  27. border: 1px solid #00ff41; font-family: monospace;
  28. }
  29. .attach-twitter-form-row1 { display:flex; align-items:center; gap:8px; }
  30. .attach-twitter-form label { color: #00ff41; font-weight: bold; }
  31. .url-wrapper { position: relative; flex: 1; display: flex; align-items: center; }
  32. #twitter-url {
  33. flex:1; background:#111; color:#00ff41; border:1px solid #00ff41;
  34. padding:6px 25px 6px 6px; border-radius:2px; outline: none;
  35. }
  36. #clear-url {
  37. position: absolute; right: 8px; cursor: pointer; color: #666;
  38. font-weight: bold; font-size: 14px; user-select: none;
  39. }
  40. #clear-url:hover { color: #ff4444; }
  41. .attach-twitter-status { font-size:12px; color:#00ff41; min-height:1.3em; margin-top: 4px; }
  42. .attach-twitter-error { color:#ff4444 !important; }
  43.  
  44. .attach-twitter { margin-left: 2px; cursor: pointer; }
  45. .do-cancel {
  46. background: transparent; color: #666; border: 1px solid #666;
  47. padding: 4px 10px; cursor: pointer; align-self: flex-end;
  48. font-family: inherit; font-size: 11px;
  49. }
  50. .do-cancel:hover { color: #fff; border-color: #fff; }
  51.  
  52. .tw-gallery {
  53. display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap;
  54. justify-content: center; background: #0a0a0a; padding: 12px;
  55. border: 1px solid #333;
  56. }
  57. .tw-item-wrap { position: relative; cursor: pointer; transition: 0.1s; }
  58. .tw-item-wrap:hover { transform: scale(1.05); }
  59. .tw-gallery-item { width: 100px; height: 100px; object-fit: cover; border: 1px solid #444; }
  60. .tw-item-wrap:hover .tw-gallery-item { border-color: #00ff41; }
  61. .tw-badge { position: absolute; bottom: 4px; right: 4px; background: #00ff41; color: #000; font-size: 10px; font-weight: bold; padding: 2px 5px; }
  62. .tw-res-badge { position: absolute; top: 4px; left: 4px; background: rgba(0,0,0,0.8); color: #00ff41; font-size: 9px; padding: 1px 4px; border: 1px solid #00ff41; }
  63. `);
  64.  
  65. const extractTweetId = (url) => url.match(/(?:twitter\.com|x\.com)\/\S+\/status\/(\d+)/i)?.[1];
  66. const sanitize = (text) => (text || "").replace(/\n/g, " ").replace(/[/\\:*?"<>|]/g, "").replace(/\s+/g, " ").trim().slice(0, 50);
  67.  
  68. function gmFetch(url, type = "text", onProgress = null) {
  69. return new Promise((resolve, reject) => {
  70. GM_xmlhttpRequest({
  71. method: "GET", url, responseType: type,
  72. onprogress: onProgress,
  73. onload: (res) => res.status === 200 ? resolve(res.response) : reject(new Error(`Status ${res.status}`)),
  74. onerror: reject
  75. });
  76. });
  77. }
  78.  
  79. async function handleAttachment(url, filename, type, fileInput, statusEl, form) {
  80. try {
  81. const data = await gmFetch(url, "arraybuffer", (p) => {
  82. if (p.lengthComputable) {
  83. const pct = Math.round((p.loaded / p.total) * 100);
  84. statusEl.textContent = `Downloading: ${pct}%`;
  85. }
  86. });
  87.  
  88. let finalData = data;
  89. if (type === "video/mp4") {
  90. const view = new DataView(data);
  91. if (view.byteLength > 16 && view.getUint32(4) === 0x66747970) {
  92. view.setUint32(8, 0x6D703432); view.setUint32(16, 0x6D703432);
  93. }
  94. }
  95.  
  96. const sizeMB = (finalData.byteLength / (1024 * 1024)).toFixed(2);
  97. if (sizeMB > MAX_SIZE_MB) throw new Error(`Too large: ${sizeMB}MB`);
  98.  
  99. const file = new File([finalData], filename, { type });
  100. const dt = new DataTransfer();
  101. dt.items.add(file);
  102. fileInput.files = dt.files;
  103. fileInput.dispatchEvent(new Event("change", { bubbles: true }));
  104. statusEl.textContent = `✅ Success: ${sizeMB}MB`;
  105. setTimeout(() => form.remove(), 1500);
  106. } catch (e) {
  107. statusEl.textContent = "Error: " + e.message;
  108. statusEl.classList.add('attach-twitter-error');
  109. }
  110. }
  111.  
  112. async function buildForm(container, fileInput) {
  113. if (container.querySelector('.attach-twitter-form')) return;
  114.  
  115. const form = document.createElement('div');
  116. form.className = 'attach-twitter-form';
  117. form.innerHTML = `
  118. <div class="attach-twitter-form-row1">
  119. <label>URL:</label>
  120. <div class="url-wrapper">
  121. <input type="text" id="twitter-url" placeholder="Paste X link...">
  122. <span id="clear-url" title="Clear">×</span>
  123. </div>
  124. <button type="button" class="do-cancel">Cancel</button>
  125. </div>
  126. <span class="attach-twitter-status">Awaiting link...</span>
  127. <div class="tw-gallery" style="display:none"></div>
  128. `;
  129.  
  130. container.appendChild(form);
  131. const urlInput = form.querySelector('#twitter-url');
  132. const statusSpan = form.querySelector('.attach-twitter-status');
  133. const gallery = form.querySelector('.tw-gallery');
  134. const clearBtn = form.querySelector('#clear-url');
  135.  
  136. const triggerFetch = async () => {
  137. const tweetId = extractTweetId(urlInput.value.trim());
  138. if (!tweetId) return;
  139.  
  140. statusSpan.textContent = "Fetching media...";
  141. gallery.innerHTML = ""; gallery.style.display = "none";
  142. statusSpan.classList.remove('attach-twitter-error');
  143.  
  144. try {
  145. const raw = await gmFetch(`https://api.fxtwitter.com/status/${tweetId}`);
  146. const { tweet } = JSON.parse(raw);
  147. const media = tweet.media?.all || [];
  148. if (!media.length) throw new Error("No media found");
  149.  
  150. const baseFilename = `@${tweet.author.screen_name} ${sanitize(tweet.text)}`.trim();
  151.  
  152. statusSpan.textContent = "Click a thumbnail to attach:";
  153. gallery.style.display = "flex";
  154.  
  155. media.forEach((m, i) => {
  156. const wrap = document.createElement('div');
  157. wrap.className = 'tw-item-wrap';
  158. wrap.innerHTML = `<img src="${m.thumbnail_url || m.url}" class="tw-gallery-item">`;
  159.  
  160. if (m.type === "video" || m.type === "gif") {
  161. const dur = m.duration ? `${Math.floor(m.duration/60)}:${(m.duration%60).toString().padStart(2,'0')}` : "GIF";
  162. const variants = m.variants || [];
  163. const best = variants.filter(v => v.content_type === "video/mp4").sort((a,b) => (b.bitrate||0) - (a.bitrate||0))[0];
  164.  
  165. // Robust resolution fix: check best variant height, then fallback to top-level height
  166. const height = best?.height || m.height || "??";
  167. wrap.innerHTML += `<span class="tw-res-badge">${height}p</span><span class="tw-badge">${dur}</span>`;
  168.  
  169. wrap.onclick = () => {
  170. gallery.style.display = "none";
  171. handleAttachment(best.url, `${baseFilename}_${i} ${tweetId}.mp4`, "video/mp4", fileInput, statusSpan, form);
  172. };
  173. } else {
  174. wrap.onclick = () => {
  175. gallery.style.display = "none";
  176. const ext = m.url.split('.').pop().split('?')[0] || 'jpg';
  177. handleAttachment(m.url, `${baseFilename}_${i} ${tweetId}.${ext}`, `image/${ext}`, fileInput, statusSpan, form);
  178. };
  179. }
  180. gallery.appendChild(wrap);
  181. });
  182. } catch (err) {
  183. statusSpan.textContent = "Error: " + err.message;
  184. statusSpan.classList.add('attach-twitter-error');
  185. }
  186. };
  187.  
  188. urlInput.addEventListener('input', triggerFetch);
  189. clearBtn.onclick = () => { urlInput.value = ""; gallery.style.display = "none"; statusSpan.textContent = "Awaiting link..."; };
  190. form.querySelector('.do-cancel').onclick = () => form.remove();
  191. urlInput.focus();
  192. }
  193.  
  194. function injectButton(uploadContainer) {
  195. if (uploadContainer.querySelector('.attach-twitter')) return;
  196. const fileInput = uploadContainer.querySelector('input[type="file"]');
  197. if (!fileInput) return;
  198. const btn = document.createElement('button');
  199. btn.className = 'attach-twitter'; btn.type = 'button'; btn.textContent = 'Attach X Video';
  200. const anchor = uploadContainer.querySelector('.attach-tiktok') || uploadContainer.querySelector('button');
  201. if (anchor) anchor.insertAdjacentElement('afterend', btn); else uploadContainer.prepend(btn);
  202. btn.onclick = () => buildForm(uploadContainer, fileInput);
  203. }
  204.  
  205. document.querySelectorAll('.upload-container').forEach(injectButton);
  206. new MutationObserver(() => document.querySelectorAll('.upload-container').forEach(injectButton)).observe(document.body, { childList: true, subtree: true });
  207. })();
Advertisement
Add Comment
Please, Sign In to add comment