ext.to → TorBox

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者にメールしてサポートを受ける。またはこのスクリプトの質問や評価の投稿はこちら、スクリプトの通報はこちらへお寄せください。
// ==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();
  }
})();