ext.to → TorBox

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

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