Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Better Meguca - X Media Import
- // @namespace Anon
- // @version 3.0
- // @description Import X Videos and Images
- // @match https://sturdychan.help/*
- // @match https://easychan.net/*
- // @match https://mokachan.cafe/*
- // @match https://noschizos.club/*
- // @grant GM_xmlhttpRequest
- // @grant GM_addStyle
- // @connect api.fxtwitter.com
- // @connect video.twimg.com
- // @connect pbs.twimg.com
- // @run-at document-idle
- // ==/UserScript==
- (async function () {
- 'use strict';
- const MAX_SIZE_MB = 100;
- GM_addStyle(`
- .attach-twitter-form {
- display:flex; flex-direction:column; gap:8px; margin-top:12px;
- background: #000 !important; padding:12px; border-radius:4px;
- border: 1px solid #00ff41; font-family: monospace;
- }
- .attach-twitter-form-row1 { display:flex; align-items:center; gap:8px; }
- .attach-twitter-form label { color: #00ff41; font-weight: bold; }
- .url-wrapper { position: relative; flex: 1; display: flex; align-items: center; }
- #twitter-url {
- flex:1; background:#111; color:#00ff41; border:1px solid #00ff41;
- padding:6px 25px 6px 6px; border-radius:2px; outline: none;
- }
- #clear-url {
- position: absolute; right: 8px; cursor: pointer; color: #666;
- font-weight: bold; font-size: 14px; user-select: none;
- }
- #clear-url:hover { color: #ff4444; }
- .attach-twitter-status { font-size:12px; color:#00ff41; min-height:1.3em; margin-top: 4px; }
- .attach-twitter-error { color:#ff4444 !important; }
- .attach-twitter { margin-left: 2px; cursor: pointer; }
- .do-cancel {
- background: transparent; color: #666; border: 1px solid #666;
- padding: 4px 10px; cursor: pointer; align-self: flex-end;
- font-family: inherit; font-size: 11px;
- }
- .do-cancel:hover { color: #fff; border-color: #fff; }
- .tw-gallery {
- display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap;
- justify-content: center; background: #0a0a0a; padding: 12px;
- border: 1px solid #333;
- }
- .tw-item-wrap { position: relative; cursor: pointer; transition: 0.1s; }
- .tw-item-wrap:hover { transform: scale(1.05); }
- .tw-gallery-item { width: 100px; height: 100px; object-fit: cover; border: 1px solid #444; }
- .tw-item-wrap:hover .tw-gallery-item { border-color: #00ff41; }
- .tw-badge { position: absolute; bottom: 4px; right: 4px; background: #00ff41; color: #000; font-size: 10px; font-weight: bold; padding: 2px 5px; }
- .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; }
- `);
- const extractTweetId = (url) => url.match(/(?:twitter\.com|x\.com)\/\S+\/status\/(\d+)/i)?.[1];
- const sanitize = (text) => (text || "").replace(/\n/g, " ").replace(/[/\\:*?"<>|]/g, "").replace(/\s+/g, " ").trim().slice(0, 50);
- function gmFetch(url, type = "text", onProgress = null) {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET", url, responseType: type,
- onprogress: onProgress,
- onload: (res) => res.status === 200 ? resolve(res.response) : reject(new Error(`Status ${res.status}`)),
- onerror: reject
- });
- });
- }
- async function handleAttachment(url, filename, type, fileInput, statusEl, form) {
- try {
- const data = await gmFetch(url, "arraybuffer", (p) => {
- if (p.lengthComputable) {
- const pct = Math.round((p.loaded / p.total) * 100);
- statusEl.textContent = `Downloading: ${pct}%`;
- }
- });
- let finalData = data;
- if (type === "video/mp4") {
- const view = new DataView(data);
- if (view.byteLength > 16 && view.getUint32(4) === 0x66747970) {
- view.setUint32(8, 0x6D703432); view.setUint32(16, 0x6D703432);
- }
- }
- const sizeMB = (finalData.byteLength / (1024 * 1024)).toFixed(2);
- if (sizeMB > MAX_SIZE_MB) throw new Error(`Too large: ${sizeMB}MB`);
- const file = new File([finalData], filename, { type });
- const dt = new DataTransfer();
- dt.items.add(file);
- fileInput.files = dt.files;
- fileInput.dispatchEvent(new Event("change", { bubbles: true }));
- statusEl.textContent = `✅ Success: ${sizeMB}MB`;
- setTimeout(() => form.remove(), 1500);
- } catch (e) {
- statusEl.textContent = "Error: " + e.message;
- statusEl.classList.add('attach-twitter-error');
- }
- }
- async function buildForm(container, fileInput) {
- if (container.querySelector('.attach-twitter-form')) return;
- const form = document.createElement('div');
- form.className = 'attach-twitter-form';
- form.innerHTML = `
- <div class="attach-twitter-form-row1">
- <label>URL:</label>
- <div class="url-wrapper">
- <input type="text" id="twitter-url" placeholder="Paste X link...">
- <span id="clear-url" title="Clear">×</span>
- </div>
- <button type="button" class="do-cancel">Cancel</button>
- </div>
- <span class="attach-twitter-status">Awaiting link...</span>
- <div class="tw-gallery" style="display:none"></div>
- `;
- container.appendChild(form);
- const urlInput = form.querySelector('#twitter-url');
- const statusSpan = form.querySelector('.attach-twitter-status');
- const gallery = form.querySelector('.tw-gallery');
- const clearBtn = form.querySelector('#clear-url');
- const triggerFetch = async () => {
- const tweetId = extractTweetId(urlInput.value.trim());
- if (!tweetId) return;
- statusSpan.textContent = "Fetching media...";
- gallery.innerHTML = ""; gallery.style.display = "none";
- statusSpan.classList.remove('attach-twitter-error');
- try {
- const raw = await gmFetch(`https://api.fxtwitter.com/status/${tweetId}`);
- const { tweet } = JSON.parse(raw);
- const media = tweet.media?.all || [];
- if (!media.length) throw new Error("No media found");
- const baseFilename = `@${tweet.author.screen_name} ${sanitize(tweet.text)}`.trim();
- statusSpan.textContent = "Click a thumbnail to attach:";
- gallery.style.display = "flex";
- media.forEach((m, i) => {
- const wrap = document.createElement('div');
- wrap.className = 'tw-item-wrap';
- wrap.innerHTML = `<img src="${m.thumbnail_url || m.url}" class="tw-gallery-item">`;
- if (m.type === "video" || m.type === "gif") {
- const dur = m.duration ? `${Math.floor(m.duration/60)}:${(m.duration%60).toString().padStart(2,'0')}` : "GIF";
- const variants = m.variants || [];
- const best = variants.filter(v => v.content_type === "video/mp4").sort((a,b) => (b.bitrate||0) - (a.bitrate||0))[0];
- // Robust resolution fix: check best variant height, then fallback to top-level height
- const height = best?.height || m.height || "??";
- wrap.innerHTML += `<span class="tw-res-badge">${height}p</span><span class="tw-badge">${dur}</span>`;
- wrap.onclick = () => {
- gallery.style.display = "none";
- handleAttachment(best.url, `${baseFilename}_${i} ${tweetId}.mp4`, "video/mp4", fileInput, statusSpan, form);
- };
- } else {
- wrap.onclick = () => {
- gallery.style.display = "none";
- const ext = m.url.split('.').pop().split('?')[0] || 'jpg';
- handleAttachment(m.url, `${baseFilename}_${i} ${tweetId}.${ext}`, `image/${ext}`, fileInput, statusSpan, form);
- };
- }
- gallery.appendChild(wrap);
- });
- } catch (err) {
- statusSpan.textContent = "Error: " + err.message;
- statusSpan.classList.add('attach-twitter-error');
- }
- };
- urlInput.addEventListener('input', triggerFetch);
- clearBtn.onclick = () => { urlInput.value = ""; gallery.style.display = "none"; statusSpan.textContent = "Awaiting link..."; };
- form.querySelector('.do-cancel').onclick = () => form.remove();
- urlInput.focus();
- }
- function injectButton(uploadContainer) {
- if (uploadContainer.querySelector('.attach-twitter')) return;
- const fileInput = uploadContainer.querySelector('input[type="file"]');
- if (!fileInput) return;
- const btn = document.createElement('button');
- btn.className = 'attach-twitter'; btn.type = 'button'; btn.textContent = 'Attach X Video';
- const anchor = uploadContainer.querySelector('.attach-tiktok') || uploadContainer.querySelector('button');
- if (anchor) anchor.insertAdjacentElement('afterend', btn); else uploadContainer.prepend(btn);
- btn.onclick = () => buildForm(uploadContainer, fileInput);
- }
- document.querySelectorAll('.upload-container').forEach(injectButton);
- new MutationObserver(() => document.querySelectorAll('.upload-container').forEach(injectButton)).observe(document.body, { childList: true, subtree: true });
- })();
Advertisement
Add Comment
Please, Sign In to add comment