Greasy Fork is available in English.

ext.to → TorBox

Send torrents from ext.to to TorBox and get a direct download link

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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();
  }
})();