Send torrents from ext.to to TorBox and get a direct download link
// ==UserScript== // @name ext.to → TorBox // @namespace https://greasyfork.org/en/users/1458606-saarmaat // @version 1.0 // @description Send torrents from ext.to to TorBox and get a direct download link // @author saarmaat // @supportURL mailto:[email protected] // @license MIT // @match https://ext.to/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @connect api.torbox.app // @connect ext.to // ==/UserScript== (function () { 'use strict'; const HARDCODED_API_KEY = ''; const API = 'https://api.torbox.app/v1/api'; const POLL_INTERVAL = 5000; const POLL_TIMEOUT = 300; function getApiKey() { if (HARDCODED_API_KEY) return HARDCODED_API_KEY; let key = GM_getValue('torbox_api_key', ''); if (!key) { key = prompt('Enter your TorBox API key (will be saved locally):'); if (key) GM_setValue('torbox_api_key', key.trim()); } return key ? key.trim() : null; } function apiRequest(method, path, data) { return new Promise((resolve, reject) => { const key = getApiKey(); if (!key) return reject(new Error('No API key')); const opts = { method, url: API + path, headers: { Authorization: `Bearer ${key}` }, onload: (r) => { try { resolve(JSON.parse(r.responseText)); } catch { reject(new Error('Bad JSON: ' + r.responseText)); } }, onerror: () => reject(new Error('Network error')), }; if (data instanceof FormData) { opts.data = data; } else if (data) { opts.headers['Content-Type'] = 'application/json'; opts.data = JSON.stringify(data); } GM_xmlhttpRequest(opts); }); } function fetchMagnet() { return new Promise((resolve, reject) => { const magnetBtn = document.querySelector('.download-btn-magnet'); if (!magnetBtn) return reject(new Error('Magnet button not found on page')); const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; const timer = setTimeout(() => { XMLHttpRequest.prototype.open = origOpen; XMLHttpRequest.prototype.send = origSend; reject(new Error('Timeout waiting for magnet response')); }, 8000); const locDescriptor = Object.getOwnPropertyDescriptor(window, 'location') || Object.getOwnPropertyDescriptor(Object.getPrototypeOf(window), 'location'); let locationBlocked = true; try { Object.defineProperty(window, 'location', { configurable: true, get: () => locDescriptor.get.call(window), set: (val) => { if (locationBlocked && typeof val === 'string' && val.startsWith('magnet:')) return; locDescriptor.set.call(window, val); }, }); } catch {} XMLHttpRequest.prototype.open = function (m, u, ...a) { this._tbUrl = u; return origOpen.call(this, m, u, ...a); }; XMLHttpRequest.prototype.send = function (...a) { if (this._tbUrl && this._tbUrl.includes('getTorrentMagnet')) { this.addEventListener('load', function () { XMLHttpRequest.prototype.open = origOpen; XMLHttpRequest.prototype.send = origSend; locationBlocked = false; try { Object.defineProperty(window, 'location', locDescriptor); } catch {} clearTimeout(timer); try { const data = JSON.parse(this.responseText); const magnet = data.url || data.magnet || data.data; if (magnet && magnet.startsWith('magnet:')) { resolve(magnet); } else { reject(new Error('Bad magnet response: ' + this.responseText.slice(0, 120))); } } catch { reject(new Error('Bad JSON from ext.to: ' + this.responseText.slice(0, 120))); } }); } return origSend.call(this, ...a); }; magnetBtn.click(); }); } function createUI() { const btn = document.createElement('button'); btn.textContent = '⬆ Send to TorBox'; Object.assign(btn.style, { display: 'inline-flex', alignItems: 'center', gap: '6px', padding: '8px 16px', marginLeft: '8px', background: '#1a73e8', color: '#fff', border: 'none', borderRadius: '6px', fontSize: '14px', fontWeight: '600', cursor: 'pointer', verticalAlign: 'middle', }); const status = document.createElement('div'); Object.assign(status.style, { marginTop: '10px', padding: '12px 16px', borderRadius: '6px', fontSize: '13px', display: 'none', maxWidth: '700px', lineHeight: '1.5', }); return { btn, status }; } function setStatus(status, type, html) { const colors = { info: { bg: '#1e3a5f', border: '#3b7bd4' }, success: { bg: '#1a3a2a', border: '#2ecc71' }, error: { bg: '#3a1a1a', border: '#e74c3c' }, }; const c = colors[type] || colors.info; Object.assign(status.style, { display: 'block', background: c.bg, border: `1px solid ${c.border}`, color: '#eee', }); status.innerHTML = html; } async function pollForReady(torrentId, status, deadline) { while (Date.now() < deadline) { await new Promise(r => setTimeout(r, POLL_INTERVAL)); const res = await apiRequest('GET', `/torrents/mylist?id=${torrentId}`); if (!res.success) throw new Error(res.detail || 'Failed to get torrent list'); const torrent = Array.isArray(res.data) ? res.data[0] : res.data; if (!torrent) throw new Error('Torrent not found in list'); const pct = torrent.progress != null ? Math.round(torrent.progress * 100) : '?'; setStatus(status, 'info', `⏳ Caching… ${pct}% — checking every ${POLL_INTERVAL / 1000}s`); if (torrent.cached || torrent.download_state === 'cached' || torrent.download_finished) { return torrent; } } throw new Error('Timed out waiting for cache'); } function pickBestFile(files) { if (!files || files.length === 0) return null; const videoExts = /\.(mp4|mkv|avi|mov|m4v|ts|wmv)$/i; const videos = files.filter(f => videoExts.test(f.name || f.short_name || '')); const pool = videos.length > 0 ? videos : files; return pool.reduce((best, f) => (f.size > best.size ? f : best), pool[0]); } async function requestDL(torrentId, torrent) { const key = getApiKey(); const files = torrent.files || []; const params = new URLSearchParams({ token: key, torrent_id: torrentId }); const best = pickBestFile(files); if (best) { params.set('file_id', best.id); } else { params.set('zip_link', 'true'); } const res = await apiRequest('GET', `/torrents/requestdl?${params}`); if (!res.success) throw new Error(res.detail || 'requestdl failed'); return res.data; } async function run(btn, status) { btn.disabled = true; btn.textContent = '⏳ Working…'; try { setStatus(status, 'info', '🔗 Fetching magnet link…'); const magnet = await fetchMagnet(); setStatus(status, 'info', '📤 Sending to TorBox…'); const form = new FormData(); form.append('magnet', magnet); form.append('seed', '1'); form.append('allow_zip', 'true'); const create = await apiRequest('POST', '/torrents/createtorrent', form); let torrentId = null; if (!create.success) { if (create.error === 'DUPLICATE_ITEM' && create.data) { torrentId = create.data.torrent_id || create.data.id; } else { throw new Error(create.detail || 'createtorrent failed'); } } else { torrentId = create.data.torrent_id || create.data.id; } if (!torrentId) throw new Error('Could not determine torrent ID'); setStatus(status, 'info', `📥 Added to TorBox (id: ${torrentId}), waiting for cache…`); const deadline = Date.now() + POLL_TIMEOUT * 1000; const torrent = await pollForReady(torrentId, status, deadline); setStatus(status, 'info', '🔑 Requesting download link…'); const dlUrl = await requestDL(torrentId, torrent); const leechUrl = dlUrl.replace(/^https:\/\//, 'leech://'); const btnStyle = 'display:inline-block;margin-top:10px;margin-right:8px;padding:7px 14px;cursor:pointer;border-radius:5px;font-size:13px;font-weight:600;border:none;'; setStatus(status, 'success', `✅ Ready!` + `<div style="margin:10px 0;word-break:break-all;font-size:12px;opacity:.6;line-height:1.4">${dlUrl}</div>` + `<div>` + `<button id="tb-copy" style="${btnStyle}background:#2980b9;color:#fff">Copy URL</button>` + `<a id="tb-leech" href="${leechUrl}" style="${btnStyle}background:#27ae60;color:#fff;text-decoration:none">Open in Leech</a>` + `</div>` ); document.getElementById('tb-copy')?.addEventListener('click', () => { navigator.clipboard.writeText(dlUrl).then(() => { document.getElementById('tb-copy').textContent = 'Copied!'; setTimeout(() => { document.getElementById('tb-copy').textContent = 'Copy URL'; }, 2000); }); }); } catch (err) { setStatus(status, 'error', `❌ ${err.message}`); } finally { btn.disabled = false; btn.textContent = '⬆ Send to TorBox'; } } function inject() { const { btn, status } = createUI(); const anchor = document.querySelector('.download-btn-file') || document.querySelector('.download-btn-magnet') || document.querySelector('#show-hash-btn'); if (anchor) { anchor.after(btn); anchor.closest('div').after(status); } else { document.body.prepend(status); document.body.prepend(btn); } btn.addEventListener('click', () => run(btn, status)); } function tryInject() { if (!document.querySelector('.download-btn-file, .download-btn-magnet, #show-hash-btn')) { setTimeout(tryInject, 500); return; } inject(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', tryInject); } else { tryInject(); } })();