Atomic TBD Auto Upload

Auto upload tool (Fixed for Violentmonkey)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Atomic TBD Auto Upload
// @namespace    TorrentBD
// @version      5.1.1
// @description  Auto upload tool (Fixed for Violentmonkey)
// @author       Atomic
// @match        https://www.torrentbd.net/torrents-upload.php
// @match        https://www.torrentbd.com/torrents-upload.php
// @match        https://www.torrentbd.me/torrents-upload.php
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      api.imgbb.com
// @connect      docs.google.com
// @connect      www.omdbapi.com
// @connect      store.steampowered.com
// @connect      www.google.com
// @connect      cdn.jsdelivr.net
// @run-at       document-end
// ==/UserScript==

(function () {
'use strict';
function loadMediaInfo() {
    return new Promise((resolve, reject) => {
        if (window.MediaInfo) {
            return resolve(window.MediaInfo);
        }

        const script = document.createElement('script');
        script.src = "https://cdn.jsdelivr.net/gh/monayemazam05/mediainfo/mediainfo.lib.js";

        script.onload = () => {
            if (window.MediaInfo) {
                resolve(window.MediaInfo);
            } else {
                reject("MediaInfo loaded but not found");
            }
        };

        script.onerror = () => reject("Failed to load MediaInfo");

        document.head.appendChild(script);
    });
}

loadMediaInfo().then((MediaInfo) => {
    console.log("MediaInfo loaded", MediaInfo);

    // your code here
}).catch(err => {
    console.error(err);
});
// =========================================================
// ⚙️ USER CONFIGURATION
// Script : Atomic TBD Auto Upload v5.0
// Developed By : Atomic
// Original Source: TBD Automate Upload 101 By Isaac_NewtonSir
// =========================================================

var youtubeApiKeys = [
  // Add your YouTube Data API v3 keys here (optional)
];

const userApiKeys = [
  '', // ← Add your ImgBB API key here
].filter(k => k.trim() !== '');

let imageSize = 'F'; // 'T' = thumb, 'M' = medium, 'F' = full

// ── Torrent Tracker ──────────────────────────────────────
const TORRENT_TRACKER     = 'https://tracker.torrentbd.net/announce';
const TORRENT_PIECE_LENGTH = 512 * 1024; // 512 KB pieces (BEP-3 standard)
const TORRENT_CREATED_BY  = 'Atomic TBD Auto Upload v5.0';

// =========================================================
// 🔧 INTERNALS
// =========================================================

const extensionsToRemove = [
  '.mkv','.mp4','.avi','.wmv','.flv','.3gp','.mov','.mpeg','.mpg',
  '.vob','.m4v','.3g2','.asf','.divx','.m2ts','.mts','.mxf','.ogg',
  '.ogv','.rm','.rmvb','.ts','.webm','.pdf','.zip','.iso',
];

const defaultMediainfo  = 'https://i.ibb.co.com/W4P5ZL70/cooltext507370943282171.png';
const defaultScreenshot = 'https://i.ibb.co.com/BKfbTBY0/cooltext507371031140727.png';
const defaultThanks     = 'https://i.ibb.co.com/DDBY0vPY/cooltext507371060290088.png';

const MediainfoImg   = defaultMediainfo;
const ScreenshotImg  = defaultScreenshot;
const ThankYouImg    = defaultThanks;
const ImageResize    = imageSize || 'M';

// =========================================================
// 🙏 THANK YOU FOOTER
// =========================================================

const videoThankYouBB = [

  '[hr]',
  '[center]',

  '[color=#F4A300][font=Orbitron]✦ ─────────────────────────────────────── ✦[/font][/color]',
  '[img]https://i.ibb.co.com/5gwyX9mP/cooltext507373515546874.png[/img]',
  '[size=4][font=Inconsolata][color=#ffe066]⚛ Happy Downloading! ⚛[/color][/font][/size]',
  '[color=#ffe066][font=Inconsolata][size=3]Keep Seeding These — spread the love and keep the swarm alive! 🌱[/size][/font][/color]',
  '[color=#F4A300][font=Orbitron]✦ ─────────────────────────────────────── ✦[/font][/color]',
  '[/center]',
  '[hr]',
].join('\n');

const tutorialThankYouBB = [
  '[hr]','[center]',
  '[color=#6c5ce7][font=Orbitron]✦ ─────────────────────────────────────── ✦[/font][/color]',
  '[img]https://i.ibb.co.com/zhppqTZp/cooltext507372209270074.png[/img]',
  '[size=4][font=Inconsolata][color=#ffe066]⚛ Happy Downloading! ⚛[/color][/font][/size]',
  '[color=#00b894][font=Inconsolata][size=3]Keep Seeding These — spread the love and keep the swarm alive! 📚[/size][/font][/color]',
  '[color=#6c5ce7][font=Orbitron]✦ ─────────────────────────────────────── ✦[/font][/color]',
  '[/center]','[hr]',
].join('\n');

const gameThankYouBB = [
  '[hr]','[center]',
  '[color=#00cec9][font=Orbitron]✦ ─────────────────────────────────────── ✦[/font][/color]',
  '[img]https://i.ibb.co.com/zhppqTZp/cooltext507372209270074.png[/img]',
  '[size=4][font=Inconsolata][color=#ffe066]⚛ Happy Downloading! ⚛[/color][/font][/size]',
  '[color=#a29bfe][font=Inconsolata][size=3]Keep Seeding These — spread the love and keep the swarm alive! 🎮[/size][/font][/color]',
  '[color=#00cec9][font=Orbitron]✦ ─────────────────────────────────────── ✦[/font][/color]',
  '[/center]','[hr]',
].join('\n');

// =========================================================
// ⏳ PROCESSING POPUP
// =========================================================

let _stepsCurrent = 0;
let _stepsTotal   = 0;
let _popupEl      = null;
let _popupStyle   = null;

function showProcessingPopup(steps) {
  hideProcessingPopup();

  _stepsCurrent = 0;
  _stepsTotal   = Array.isArray(steps) ? steps.length : 0;

  const style = document.createElement('style');
  style.id = 'atomic-popup-style';
  style.textContent = `
    @keyframes atomic-spin    { to { transform: rotate(360deg); } }
    @keyframes atomic-pulse   { 0%,100%{opacity:.35}50%{opacity:1} }
    @keyframes atomic-shimmer { 0%{background-position:200% center}100%{background-position:-200% center} }
    @keyframes atomic-fadein  { from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)} }
    @keyframes atomic-stepin  { from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)} }
    #atomic-processing {
      position:fixed;inset:0;
      background:rgba(4,7,18,0.88);
      display:flex;flex-direction:column;align-items:center;justify-content:center;
      z-index:99999;gap:0;backdrop-filter:blur(6px);
    }
    .atomic-card {
      background:linear-gradient(145deg,#0d1a2e,#0a1020);
      border:1px solid rgba(0,207,255,.2);
      border-radius:20px;padding:36px 40px;min-width:340px;max-width:460px;
      box-shadow:0 0 60px rgba(0,207,255,.12),0 20px 60px rgba(0,0,0,.6);
      animation:atomic-fadein .3s ease forwards;
    }
    .atomic-spinner-ring {
      width:56px;height:56px;border-radius:50%;
      border:3px solid rgba(0,207,255,.12);
      border-top-color:#00cfff;
      animation:atomic-spin .75s linear infinite;
    }
    .atomic-title {
      font-family:'Bahnschrift','Segoe UI',sans-serif;
      font-size:1.05rem;font-weight:800;letter-spacing:.06em;
      background:linear-gradient(90deg,#a78bfa,#00cfff,#34d399);
      background-size:200% auto;
      -webkit-background-clip:text;-webkit-text-fill-color:transparent;
      animation:atomic-shimmer 3s linear infinite;
      margin:0;
    }
    .atomic-subtitle {
      font-family:'Segoe UI',sans-serif;
      font-size:.68rem;color:rgba(255,255,255,.35);
      letter-spacing:.03em;margin-top:2px;
    }
    .atomic-step-label {
      font-family:'Segoe UI',sans-serif;
      font-size:.78rem;color:rgba(255,255,255,.5);
      letter-spacing:.04em;margin-top:4px;min-height:1.2em;
      animation:atomic-stepin .25s ease;
    }
    .atomic-counter { font-family:'Bahnschrift',monospace;font-size:.72rem;letter-spacing:.08em;color:rgba(0,207,255,.6); }
    .atomic-bar-track { width:100%;height:6px;border-radius:4px;background:rgba(255,255,255,.06);overflow:hidden;margin-top:8px; }
    .atomic-bar-fill {
      height:100%;border-radius:4px;
      background:linear-gradient(90deg,#7c3aed,#00cfff,#34d399);
      background-size:200% auto;
      transition:width .4s cubic-bezier(.4,0,.2,1);
      animation:atomic-shimmer 2s linear infinite;
    }
    .atomic-step-list { margin-top:16px;list-style:none;padding:0;display:flex;flex-direction:column;gap:5px; }
    .atomic-step-item {
      display:flex;align-items:center;gap:8px;
      font-family:'Segoe UI',sans-serif;font-size:.76rem;
      color:rgba(255,255,255,.35);transition:color .3s;padding:2px 0;
    }
    .atomic-step-item.done   { color:rgba(52,211,153,.9); }
    .atomic-step-item.active { color:rgba(0,207,255,.9);animation:atomic-pulse 1.5s infinite; }
    .atomic-step-dot { width:7px;height:7px;border-radius:50%;flex-shrink:0;background:rgba(255,255,255,.12);transition:background .3s,box-shadow .3s; }
    .atomic-step-item.done   .atomic-step-dot { background:#34d399;box-shadow:0 0 6px #34d399; }
    .atomic-step-item.active .atomic-step-dot { background:#00cfff;box-shadow:0 0 8px #00cfff; }
  `;
  document.head.appendChild(style);
  _popupStyle = style;

  const pop = document.createElement('div');
  pop.id = 'atomic-processing';

  const stepItems = Array.isArray(steps)
    ? steps.map((s, i) => `<li class="atomic-step-item" id="atomic-step-${i}">
        <span class="atomic-step-dot"></span>
        <span>${s}</span>
      </li>`).join('')
    : '';

  const pct = _stepsTotal ? 0 : 100;
  pop.innerHTML = `
    <div class="atomic-card">
      <div style="display:flex;align-items:center;gap:16px;margin-bottom:20px;">
        <div class="atomic-spinner-ring"></div>
        <div>
          <p class="atomic-title">⚛ ATOMIC TBD AUTO UPLOAD</p>
          <p class="atomic-subtitle">Developed By Atomic · v5.0</p>
          <p class="atomic-step-label" id="atomic-step-label">Initialising…</p>
        </div>
      </div>
      <div style="display:flex;justify-content:space-between;align-items:center;">
        <span class="atomic-counter" id="atomic-counter">${_stepsTotal ? '0 / ' + _stepsTotal + ' done' : 'Working…'}</span>
        <span class="atomic-counter" id="atomic-pct">0%</span>
      </div>
      <div class="atomic-bar-track">
        <div class="atomic-bar-fill" id="atomic-bar" style="width:${pct}%"></div>
      </div>
      ${stepItems ? `<ul class="atomic-step-list">${stepItems}</ul>` : ''}
    </div>
  `;

  document.body.appendChild(pop);
  _popupEl = pop;
}

function advanceStep(label) {
  if (!_popupEl) return;
  const prev = _stepsCurrent - 1;
  if (prev >= 0) {
    const prevEl = document.getElementById(`atomic-step-${prev}`);
    if (prevEl) { prevEl.classList.remove('active'); prevEl.classList.add('done'); }
  }
  const cur = document.getElementById(`atomic-step-${_stepsCurrent}`);
  if (cur) cur.classList.add('active');
  _stepsCurrent++;

  const pct     = _stepsTotal ? Math.round(((_stepsCurrent - 1) / _stepsTotal) * 100) : 50;
  const bar     = document.getElementById('atomic-bar');
  const counter = document.getElementById('atomic-counter');
  const pctEl   = document.getElementById('atomic-pct');
  const lblEl   = document.getElementById('atomic-step-label');

  if (bar)     bar.style.width = pct + '%';
  if (counter) counter.textContent = (_stepsCurrent - 1) + ' / ' + _stepsTotal + ' done';
  if (pctEl)   pctEl.textContent = pct + '%';
  if (lblEl) {
    lblEl.style.animation = 'none';
    void lblEl.offsetHeight;
    lblEl.style.animation = '';
    lblEl.textContent = label || 'Processing…';
  }
}

function finishPopup(steps) {
  const bar     = document.getElementById('atomic-bar');
  const counter = document.getElementById('atomic-counter');
  const pctEl   = document.getElementById('atomic-pct');
  const lastStep = document.getElementById(`atomic-step-${steps.length - 1}`);
  if (bar)      bar.style.width = '100%';
  if (counter)  counter.textContent = steps.length + ' / ' + steps.length + ' done';
  if (pctEl)    pctEl.textContent = '100%';
  if (lastStep) { lastStep.classList.remove('active'); lastStep.classList.add('done'); }
}

function hideProcessingPopup() {
  document.getElementById('atomic-processing')?.remove();
  document.getElementById('atomic-popup-style')?.remove();
  _popupEl    = null;
  _popupStyle = null;
}

// =========================================================
// 📤 IMAGE UPLOAD — ImgBB
// =========================================================

async function fetchApiKeys() {
  try {
    const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/1HkXvYnsNGr65iob781-sv3bMari51z6eSLUm8l2vOZo/gviz/tq?tqx=out:json';
    const response = await fetch(SPREADSHEET_URL);
    const text = await response.text();
    const json = JSON.parse(text.substr(47).slice(0, -2));
    return json.table.rows.map(row => row.c[0].v).filter(Boolean);
  } catch (e) {
    console.warn('[Atomic TBD] Could not fetch shared API keys:', e);
    return [];
  }
}

async function uploadImages(images, apiKeys, sizes = ImageResize) {
  if (!apiKeys || apiKeys.length === 0) {
    console.warn('[Atomic TBD] No API keys — inserting placeholder.');
    return images.map(() => ({ fullUrl: 'UPLOAD_MANUALLY', resizeUrl: 'UPLOAD_MANUALLY' }));
  }
  const links = [];
  for (const image of images) {
    let uploaded = false;
    for (const apiKey of apiKeys) {
      const fd = new FormData();
      fd.append('image', image, 'screenshot.png');
      fd.append('key', apiKey);
      fd.append('expiration', '0');
      try {
        const res  = await fetch('https://api.imgbb.com/1/upload', { method: 'POST', body: fd });
        const json = await res.json();
        if (json.success) {
          const full   = json.data.url;
          const resize = sizes === 'T' ? (json.data?.thumb?.url ?? full)
                       : sizes === 'M' ? (json.data?.medium?.url ?? full)
                       : full;
          links.push({ fullUrl: full, resizeUrl: resize });
          uploaded = true;
          break;
        }
      } catch (e) {
        console.error('[Atomic TBD] Upload error with key', apiKey, e);
      }
    }
    if (!uploaded) links.push({ fullUrl: 'UPLOAD_MANUALLY', resizeUrl: 'UPLOAD_MANUALLY' });
  }
  return links;
}

// =========================================================
// 📸 BUILD SCREENSHOT BBCODE
// =========================================================

async function buildScreenshotBBCode(imageLinks) {
  const header = [
    '[hr]',
    '[center]',
    '[color=#C00014][font=Orbitron]◈ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◈[/font][/color]',
    '[img]https://i.ibb.co.com/wZfk3R99/cooltext507373214319597.png[/img]',
    '[color=#C00014][font=Orbitron]◈ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◈[/font][/color]',
    '[/center]',
    '[hr]',
    '',
  ].join('\n');

  const allCodes = imageLinks.map(l =>
    l.fullUrl === 'UPLOAD_MANUALLY'
      ? '[img]UPLOAD_MANUALLY[/img] <!-- replace manually -->'
      : `[url=${l.fullUrl}][img]${l.resizeUrl}[/img][/url]`
  );
  const body = `[center]\n${allCodes.join('\n\n')}\n[/center]`;
  return header + body + '\n' + videoThankYouBB;
}

// =========================================================
// 🎬 SCREENSHOT GENERATION
// =========================================================

async function generateScreenshots(videoFile) {
  return new Promise((resolve, reject) => {
    const video = document.createElement('video');
    video.src         = URL.createObjectURL(videoFile);
    video.crossOrigin = 'anonymous';
    video.muted       = true;
    video.preload     = 'auto';

    video.addEventListener('loadedmetadata', async () => {
      const d = video.duration;
      if (!d || !isFinite(d) || d < 2) { reject(new Error('Video duration invalid')); return; }
      const times  = [d * 0.31, d * 0.51, d * 0.71, d * 0.81];
      const canvas = document.createElement('canvas');
      const ctx    = canvas.getContext('2d');
      const shots  = [];

      for (const t of times) {
        try {
          await new Promise((res, rej) => {
            const timeout = setTimeout(() => rej(new Error('seek timeout')), 8000);
            video.currentTime = t;
            video.addEventListener('seeked', () => {
              clearTimeout(timeout);
              try {
                canvas.width  = video.videoWidth  || 1280;
                canvas.height = video.videoHeight || 720;
                ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
                canvas.toBlob(blob => { if (blob) shots.push(blob); res(); }, 'image/png');
              } catch (e) { rej(e); }
            }, { once: true });
          });
        } catch (e) {
          console.warn(`[Atomic TBD] Skipped frame at ${t.toFixed(1)}s:`, e.message);
        }
      }
      URL.revokeObjectURL(video.src);
      resolve(shots);
    });

    video.addEventListener('error', e => reject(new Error('Video load error: ' + e.message)));
    video.load();
  });
}

// =========================================================
// 📋 MEDIAINFO PROCESSING
// =========================================================

async function processMediaInfo(file) {
  const steps = [
    '📂 Reading file metadata',
    '🔬 Analysing MediaInfo',
    '🎬 Capturing screenshots',
    '☁️ Uploading screenshots',
    '🖊️ Building BBCode',
  ];
  showProcessingPopup(steps);
  advanceStep(steps[0]);

  const getSize   = () => file.size;
  const readChunk = (chunkSize, offset) =>
    new Promise((res, rej) => {
      const reader = new FileReader();
      reader.onload = e => e.target.error ? rej(e.target.error) : res(new Uint8Array(e.target.result));
      reader.readAsArrayBuffer(file.slice(offset, offset + chunkSize));
    });

  const miHeader = [
    '[hr]',
    '[center]',
    '[color=#80AEB7][font=Orbitron]◆ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◆[/font][/color]',
    '[img]https://i.ibb.co.com/ynQWq0ZS/cooltext507372835659325.png[/img]',
    '[color=#80AEB7][font=Orbitron]◆ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◆[/font][/color]',
    '[/center]',
    '[hr]',
  ].join('\n') + '\n';

  let mediaInfoResult = '';

  try {
    advanceStep(steps[1]);
    const mi  = await MediaInfo({ format: 'text' });
    const raw = await mi.analyzeData(getSize, readChunk);

    const genIdx   = raw.indexOf('\nGeneral');
    const nextIdx  = raw.indexOf('\n', genIdx + 1);
    const nameLine = '\nComplete name                            : ' + file.name;
    mediaInfoResult = raw.substring(0, nextIdx) + nameLine + raw.substring(nextIdx);

    const langMatch = raw.match(/Language\s*:\s*(.*)/);
    if (langMatch) {
      const langMap = {
        'English':'1','Hindi':'3','Arabic':'18','Bengali':'8','Bulgarian':'14',
        'Chinese':'5','Czech':'15','Danish':'24','Dutch':'25','Filipino':'16',
        'Finnish':'26','French':'2','German':'9','Greek':'27','Hebrew':'28',
        'Hungarian':'17','Icelandic':'30','Indonesian':'31','Irish':'32',
        'Italian':'12','Japanese':'7','Kannada':'41','Korean':'10',
        'Malayalam':'33','Marathi':'34','Norwegian':'35','Panjabi':'43',
        'Persian':'36','Polish':'37','Portuguese':'38','Romanian':'39',
        'Russian':'13','Serbian':'19','Spanish':'6','Swedish':'20',
        'Tamil':'21','Telugu':'11','Thai':'40','Turkish':'22',
        'Urdu':'4','Vietnamese':'23',
      };
      const val = langMap[langMatch[1].trim()] || '0';
      const sel = document.querySelector('select[name="lang"]');
      if (sel && (sel.value === '0' || sel.options[sel.selectedIndex].text === 'Choose Language')) {
        sel.value = val;
      }
    }

    advanceStep(steps[2]);
    const shots = await generateScreenshots(file);

    advanceStep(steps[3]);
    const keys   = userApiKeys.length ? userApiKeys : await fetchApiKeys();
    const iLinks = await uploadImages(shots, keys);

    advanceStep(steps[4]);
    const imgCode = await buildScreenshotBBCode(iLinks);

    finishPopup(steps);

    const miBB  = `[b][size=3][font=Share Tech Mono][mediainfo]\n${mediaInfoResult}\n[/mediainfo][/b][/font][/size]`;
    const field = document.getElementById('torr-descr');
    if (field) field.value = field.value + '\n' + miHeader + miBB + '\n\n' + imgCode;

  } catch (err) {
    console.error('[Atomic TBD] MediaInfo error:', err);
    const miBB  = `[b][size=3][font=Share Tech Mono][mediainfo]\n${mediaInfoResult}\n[/mediainfo][/b][/font][/size]`;
    const field = document.getElementById('torr-descr');
    if (field) field.value = field.value + '\n\n' + miHeader + miBB + '\n\n' + videoThankYouBB;
  } finally {
    setTimeout(hideProcessingPopup, 600);
  }
}

// =========================================================
// 🧲 BENCODE ENCODER (BEP-3)
// Handles: Uint8Array (binary), string, number, Array, Object
// Keys in objects are sorted lexicographically per spec
// =========================================================

function bencodeEncode(value) {
  const enc = new TextEncoder();

  function encodeValue(v) {
    if (v instanceof Uint8Array) {
      const lenBytes = enc.encode(String(v.length) + ':');
      const out = new Uint8Array(lenBytes.length + v.length);
      out.set(lenBytes, 0); out.set(v, lenBytes.length);
      return out;
    } else if (typeof v === 'string') {
      const strBytes = enc.encode(v);
      const lenBytes = enc.encode(String(strBytes.length) + ':');
      const out = new Uint8Array(lenBytes.length + strBytes.length);
      out.set(lenBytes, 0); out.set(strBytes, lenBytes.length);
      return out;
    } else if (typeof v === 'number') {
      return enc.encode('i' + Math.floor(v) + 'e');
    } else if (Array.isArray(v)) {
      return concatUint8Arrays([enc.encode('l'), ...v.map(encodeValue), enc.encode('e')]);
    } else if (v !== null && typeof v === 'object') {
      const keys  = Object.keys(v).sort();
      const parts = [enc.encode('d')];
      for (const k of keys) { parts.push(encodeValue(k)); parts.push(encodeValue(v[k])); }
      parts.push(enc.encode('e'));
      return concatUint8Arrays(parts);
    }
    throw new Error('[Atomic TBD] bencodeEncode: unsupported type ' + typeof v);
  }

  return encodeValue(value);
}

function concatUint8Arrays(arrays) {
  const total = arrays.reduce((s, a) => s + a.length, 0);
  const out   = new Uint8Array(total);
  let offset  = 0;
  for (const a of arrays) { out.set(a, offset); offset += a.length; }
  return out;
}

// =========================================================
// 🔢 SHA-1 PIECE HASHING
// =========================================================

/**
 * Hash pieces from a SINGLE file.
 * Returns Uint8Array of concatenated 20-byte SHA-1 digests.
 */
async function computePiecesFromFile(file, pieceLength, onProgress) {
  const numPieces = Math.ceil(file.size / pieceLength);
  const allHashes = new Uint8Array(numPieces * 20);
  for (let i = 0; i < numPieces; i++) {
    const start = i * pieceLength;
    const end   = Math.min(start + pieceLength, file.size);
    const chunk = await file.slice(start, end).arrayBuffer();
    const hash  = await crypto.subtle.digest('SHA-1', chunk);
    allHashes.set(new Uint8Array(hash), i * 20);
    if (onProgress) onProgress(i + 1, numPieces);
  }
  return allHashes;
}

/**
 * Hash pieces from MULTIPLE files (folder torrent).
 * Data is treated as one continuous byte stream concatenated in file order (BEP-3).
 * Returns Uint8Array of concatenated 20-byte SHA-1 digests.
 */
async function computePiecesFromFiles(files, totalSize, pieceLength, onProgress) {
  const numPieces = Math.ceil(totalSize / pieceLength);
  const allHashes = new Uint8Array(numPieces * 20);

  // We maintain a rolling buffer across file boundaries
  let bufferChunks = []; // Array<Uint8Array>
  let bufferSize   = 0;
  let pieceIndex   = 0;
  let filesDone    = 0;

  for (const file of files) {
    let fileOffset = 0;
    while (fileOffset < file.size) {
      const needed   = pieceLength - bufferSize;
      const end      = Math.min(fileOffset + needed, file.size);
      const chunk    = new Uint8Array(await file.slice(fileOffset, end).arrayBuffer());
      bufferChunks.push(chunk);
      bufferSize  += chunk.length;
      fileOffset  += chunk.length;

      if (bufferSize >= pieceLength) {
        // Concatenate buffer and hash
        const pieceData = concatUint8Arrays(bufferChunks);
        const hash      = await crypto.subtle.digest('SHA-1', pieceData);
        allHashes.set(new Uint8Array(hash), pieceIndex * 20);
        pieceIndex++;
        bufferChunks = [];
        bufferSize   = 0;
        if (onProgress) onProgress(pieceIndex, numPieces);
      }
    }
    filesDone++;
  }

  // Hash any remaining bytes (final partial piece)
  if (bufferSize > 0) {
    const pieceData = concatUint8Arrays(bufferChunks);
    const hash      = await crypto.subtle.digest('SHA-1', pieceData);
    allHashes.set(new Uint8Array(hash), pieceIndex * 20);
    pieceIndex++;
    if (onProgress) onProgress(pieceIndex, numPieces);
  }

  return allHashes;
}

// =========================================================
// 📄 SINGLE-FILE TORRENT CREATOR
// =========================================================

async function createSingleFileTorrentBlob(file, onProgress) {
  const pieces = await computePiecesFromFile(file, TORRENT_PIECE_LENGTH, onProgress);
  const info   = {
    length: file.size,
    name:   file.name,
    'piece length': TORRENT_PIECE_LENGTH,
    pieces: pieces,
  };
  const torrent = {
    announce:       TORRENT_TRACKER,
    'announce-list': [[TORRENT_TRACKER]],
    comment:        'Created by Atomic TBD Auto Upload v5.0',
    'created by':   TORRENT_CREATED_BY,
    'creation date': Math.floor(Date.now() / 1000),
    encoding:       'UTF-8',
    info:           info,
  };
  return new Blob([bencodeEncode(torrent)], { type: 'application/x-bittorrent' });
}

// =========================================================
// 📁 MULTI-FILE (FOLDER) TORRENT CREATOR
// =========================================================

/**
 * Builds a multi-file BEP-3 .torrent from a FileList (from webkitdirectory).
 * @param {FileList} fileList   — from <input webkitdirectory>
 * @param {string}   folderName — root folder name for the torrent
 * @param {Function} onProgress — (piecesDone, totalPieces)
 * @param {Function} onFileScan — (fileIndex, totalFiles)
 */
async function createFolderTorrentBlob(fileList, folderName, onProgress, onFileScan) {
  // Sort files deterministically by path
  const files = Array.from(fileList).sort((a, b) => a.webkitRelativePath.localeCompare(b.webkitRelativePath));
  const totalSize = files.reduce((s, f) => s + f.size, 0);

  // Build file list entries for 'info.files'
  // Each entry: { length, path: [dir, ..., filename] }
  // We strip the root folder from the path (it becomes the 'name' key)
  const fileEntries = files.map(f => {
    const parts = f.webkitRelativePath.split('/');
    // parts[0] is the root folder — skip it
    const pathParts = parts.slice(1);
    return { length: f.size, path: pathParts };
  });

  const pieces = await computePiecesFromFiles(files, totalSize, TORRENT_PIECE_LENGTH, onProgress);

  const info = {
    files:          fileEntries,
    name:           folderName,
    'piece length': TORRENT_PIECE_LENGTH,
    pieces:         pieces,
  };

  const torrent = {
    announce:        TORRENT_TRACKER,
    'announce-list': [[TORRENT_TRACKER]],
    comment:         'Created by Atomic TBD Auto Upload v5.0',
    'created by':    TORRENT_CREATED_BY,
    'creation date': Math.floor(Date.now() / 1000),
    encoding:        'UTF-8',
    info:            info,
  };

  return new Blob([bencodeEncode(torrent)], { type: 'application/x-bittorrent' });
}

// =========================================================
// 💉 INJECT .TORRENT INTO UPLOAD FORM
// =========================================================

function injectTorrentFile(torrentFile) {
  const input = document.querySelector('input[type="file"][name="torrent"]');
  if (!input) {
    console.error('[Atomic TBD] Could not find torrent file input.');
    return false;
  }
  try {
    const dt = new DataTransfer();
    dt.items.add(torrentFile);
    input.files = dt.files;
    input.dispatchEvent(new Event('change', { bubbles: true }));
    input.dispatchEvent(new Event('input',  { bubbles: true }));
    return true;
  } catch (e) {
    console.error('[Atomic TBD] DataTransfer injection failed:', e);
    return false;
  }
}

// =========================================================
// 🔨 CREATE TORRENT BUTTON GROUP (v5.0)
// Two buttons side-by-side:
//   [📄 File Torrent]   — single file picker (original behaviour)
//   [📁 Folder Torrent] — directory picker  (NEW in v5.0)
// =========================================================

function createTorrentButtons() {
  // ── Wrapper ──────────────────────────────────────────
  const wrap = document.createElement('div');
  Object.assign(wrap.style, {
    display:    'inline-flex',
    alignItems: 'center',
    gap:        '0',
    marginRight:'10px',
    marginLeft: '8px',
    borderRadius:'8px',
    overflow:   'hidden',
    boxShadow:  '0 2px 12px rgba(108,92,231,0.3)',
  });

  // ── Shared button factory ────────────────────────────
  function makeBtn(label, icon, gradient, title) {
    const btn = document.createElement('button');
    btn.type  = 'button';
    btn.title = title;
    btn.style.cssText = `
      display:inline-flex;align-items:center;gap:6px;
      padding:0 14px;height:36px;border:none;cursor:pointer;
      font-family:'Bahnschrift','Segoe UI',sans-serif;font-size:.8rem;font-weight:700;
      color:#fff;background:${gradient};
      transition:filter .15s;white-space:nowrap;
    `;
    btn.onmouseenter = () => btn.style.filter = 'brightness(1.15)';
    btn.onmouseleave = () => btn.style.filter = '';
    btn.innerHTML = `<span>${icon}</span><span>${label}</span>`;
    return btn;
  }

  const fileBtn   = makeBtn('File Torrent',   '📄', 'linear-gradient(135deg,#6c5ce7,#00cec9)', 'Create .torrent from a single file');
  const folderBtn = makeBtn('Folder Torrent', '📁', 'linear-gradient(135deg,#00b894,#00cec9)', 'Create .torrent from an entire folder (multi-file BEP-3)');

  // Thin divider between the two buttons
  const div = document.createElement('div');
  div.style.cssText = 'width:1px;height:36px;background:rgba(255,255,255,0.15);flex-shrink:0;';

  wrap.append(fileBtn, div, folderBtn);

  // ── Hidden file inputs ───────────────────────────────
  const fileInput   = document.createElement('input');
  fileInput.type    = 'file';
  fileInput.accept  = '*/*';
  fileInput.style.cssText = 'display:none;';

  const folderInput = document.createElement('input');
  folderInput.type  = 'file';
  folderInput.setAttribute('webkitdirectory', '');
  folderInput.setAttribute('directory', '');
  folderInput.multiple = true;
  folderInput.style.cssText = 'display:none;';

  document.body.appendChild(fileInput);
  document.body.appendChild(folderInput);

  // ── File button handler ──────────────────────────────
  fileBtn.addEventListener('click', () => fileInput.click());

  fileInput.addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const steps = [
      '📂 Reading file info',
      '🔢 Hashing pieces (SHA-1)',
      '🧱 Building .torrent structure',
      '💾 Injecting into upload form',
    ];
    showProcessingPopup(steps);
    advanceStep(steps[0]);
    await new Promise(r => setTimeout(r, 80));

    try {
      advanceStep(steps[1]);

      const torrentBlob = await createSingleFileTorrentBlob(file, (done, total) => {
        const pct   = Math.round((done / total) * 100);
        const lblEl = document.getElementById('atomic-step-label');
        if (lblEl) lblEl.textContent = `Hashing pieces… ${done}/${total} (${pct}%)`;
        const bar   = document.getElementById('atomic-bar');
        if (bar) bar.style.width = (10 + Math.round((done / total) * 70)) + '%';
      });

      advanceStep(steps[2]);
      await new Promise(r => setTimeout(r, 60));

      const torrentFileName = file.name.replace(/\.[^.]+$/, '') + '.torrent';
      const torrentFile = new File([torrentBlob], torrentFileName, {
        type: 'application/x-bittorrent', lastModified: Date.now(),
      });

      advanceStep(steps[3]);
      await new Promise(r => setTimeout(r, 60));

      const ok = injectTorrentFile(torrentFile);

      // Auto-fill name field
      const nameField = document.querySelector('#torrent_name');
      if (nameField && !nameField.value.trim()) {
        const baseName = removeExtensions(file.name.replace(/\.[^.]+$/, ''));
        nameField.value = baseName.trim();
        nameField.dispatchEvent(new Event('input', { bubbles: true }));
      }

      finishPopup(steps);
      await new Promise(r => setTimeout(r, 800));
      hideProcessingPopup();

      if (ok) {
        showToast(`✅ File .torrent created & loaded!\n📁 ${torrentFileName}\n🌐 Tracker: ${TORRENT_TRACKER}`, 'success');
      } else {
        showToast('⚠️ .torrent created but could not be injected automatically.', 'warn');
        downloadBlob(torrentBlob, torrentFileName);
      }
    } catch (err) {
      console.error('[Atomic TBD] File torrent error:', err);
      hideProcessingPopup();
      showToast('❌ Failed to create .torrent: ' + err.message, 'error');
    }
    fileInput.value = '';
  });

  // ── Folder button handler ────────────────────────────
  folderBtn.addEventListener('click', () => folderInput.click());

  folderInput.addEventListener('change', async (e) => {
    const files = e.target.files;
    if (!files || files.length === 0) return;

    // Derive folder name from the first file's webkitRelativePath
    const rootFolder = files[0].webkitRelativePath.split('/')[0] || 'folder';
    const totalFiles = files.length;
    const totalSize  = Array.from(files).reduce((s, f) => s + f.size, 0);
    const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(1);

    const steps = [
      '📂 Scanning folder contents',
      '🔢 Hashing pieces (SHA-1)',
      '🧱 Building multi-file .torrent',
      '💾 Injecting into upload form',
    ];
    showProcessingPopup(steps);
    advanceStep(steps[0]);

    // Show scan info immediately
    const lblEl0 = document.getElementById('atomic-step-label');
    if (lblEl0) lblEl0.textContent = `Found ${totalFiles} files · ${totalSizeMB} MB`;
    await new Promise(r => setTimeout(r, 400));

    try {
      advanceStep(steps[1]);

      const torrentBlob = await createFolderTorrentBlob(
        files,
        rootFolder,
        (piecesDone, totalPieces) => {
          const pct   = Math.round((piecesDone / totalPieces) * 100);
          const lblEl = document.getElementById('atomic-step-label');
          if (lblEl) lblEl.textContent = `Hashing pieces… ${piecesDone}/${totalPieces} (${pct}%)`;
          const bar   = document.getElementById('atomic-bar');
          if (bar) bar.style.width = (10 + Math.round((piecesDone / totalPieces) * 70)) + '%';
        }
      );

      advanceStep(steps[2]);
      await new Promise(r => setTimeout(r, 80));

      const torrentFileName = rootFolder + '.torrent';
      const torrentFile = new File([torrentBlob], torrentFileName, {
        type: 'application/x-bittorrent', lastModified: Date.now(),
      });

      advanceStep(steps[3]);
      await new Promise(r => setTimeout(r, 60));

      const ok = injectTorrentFile(torrentFile);

      // Auto-fill name field with folder name
      const nameField = document.querySelector('#torrent_name');
      if (nameField && !nameField.value.trim()) {
        nameField.value = rootFolder.trim();
        nameField.dispatchEvent(new Event('input', { bubbles: true }));
      }

      finishPopup(steps);
      await new Promise(r => setTimeout(r, 800));
      hideProcessingPopup();

      if (ok) {
        showToast(
          `✅ Folder .torrent created & loaded!\n📁 ${torrentFileName}\n📂 ${totalFiles} files · ${totalSizeMB} MB\n🌐 Tracker: ${TORRENT_TRACKER}`,
          'success'
        );
      } else {
        showToast('⚠️ .torrent created but could not be injected — downloading instead.', 'warn');
        downloadBlob(torrentBlob, torrentFileName);
      }
    } catch (err) {
      console.error('[Atomic TBD] Folder torrent error:', err);
      hideProcessingPopup();
      showToast('❌ Failed to create folder .torrent: ' + err.message, 'error');
    }
    folderInput.value = '';
  });

  return wrap;
}

// =========================================================
// 🔔 TOAST NOTIFICATION
// =========================================================

function showToast(message, type = 'info') {
  document.getElementById('atomic-toast')?.remove();
  const colors = { success:'#00b894', warn:'#fdcb6e', error:'#d63031', info:'#0984e3' };
  const toast  = document.createElement('div');
  toast.id = 'atomic-toast';
  Object.assign(toast.style, {
    position:'fixed', bottom:'24px', right:'24px',
    background:'#0d1a2e', border:`1px solid ${colors[type]||colors.info}`,
    borderLeft:`5px solid ${colors[type]||colors.info}`,
    color:'#e2e8f0', padding:'14px 20px', borderRadius:'10px',
    zIndex:'999999', maxWidth:'420px',
    fontFamily:"'Segoe UI',sans-serif", fontSize:'0.82rem',
    lineHeight:'1.5', whiteSpace:'pre-line',
    boxShadow:'0 8px 30px rgba(0,0,0,0.5)',
    transition:'opacity 0.4s', opacity:'0',
  });
  toast.textContent = message;
  document.body.appendChild(toast);
  requestAnimationFrame(() => { toast.style.opacity = '1'; });
  setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); }, 6000);
}

function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a   = document.createElement('a');
  a.href = url; a.download = filename; a.style.display = 'none';
  document.body.appendChild(a); a.click();
  setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 2000);
}

// =========================================================
// 🎥 VIDEO UPLOAD BUTTON (MediaInfo + Screenshots)
// =========================================================

function createVideoButton() {
  const btn = document.createElement('button');
  btn.className = 'btn';
  btn.style.cssText = `
   margin-right:10px;
   margin-left:8px;
   background: linear-gradient(135deg, #6c5ce7, #00cec9);
   color: #fff;
   border: none;
`;
  btn.innerHTML = '<i class="material-icons right">video_library</i> Metainfo Uploader';
  btn.type = 'button';

  const fi      = document.createElement('input');
  fi.type       = 'file';
  fi.accept     = 'video/*,.mkv';
  fi.style.cssText = 'opacity:0;position:absolute;z-index:-1;';
  btn.appendChild(fi);

  btn.addEventListener('click', () => fi.click());
  fi.addEventListener('change', async e => {
    const f = e.target.files[0];
    if (f) await processMediaInfo(f);
  });
  return btn;
}

function insertButtons() {
  const anchor = document.querySelector('.preview-trigger.teal.darken-3');
  if (anchor) {
    // Order (left → right): [Create Torrent group] [Select Video] [anchor…]
    anchor.parentNode.insertBefore(createVideoButton(),   anchor);
    anchor.parentNode.insertBefore(createTorrentButtons(), anchor);
  }
}

window.addEventListener('load', insertButtons);

// =========================================================
// 🏷️ CATEGORY AUTO-SELECTION
// =========================================================

const OMDb_KEYS   = ['9990cb2f','79ee18d','bc90b6c5'];
let omdbKeyIndex  = 0;
let lastCat       = '';
let manualCat     = false;
let titleEditing  = false;
let isDocumentary = false;
let autoMode      = true;
let categoryPollId = null;

function nextOmdbKey() {
  const k = OMDb_KEYS[omdbKeyIndex];
  omdbKeyIndex = (omdbKeyIndex + 1) % OMDb_KEYS.length;
  return k;
}

function selectCategory(val) {
  const s = document.querySelector('#filter');
  if (s) { s.value = val; s.dispatchEvent(new Event('change')); }
}

const RX = {
  WEBRip  : /(Webrip|WebRip|WEBRip|WEBRIP|WEBRiP|DS4K|WEB[\s-]?Rip)/i,
  WEBDL   : /(WEB-DL|WEBDL|WEB[\s-]?DL)/i,
  Remux   : /(Remux|REMUX|ReMux)/i,
  BluRay  : /(BluRay|blu-ray|BLURAY|Blu-Ray|BRrip|BRRIP|BR[\s-]?RIP|SDRip)/i,
  BD3D    : /(3D|3 D)/i,
  HDRip   : /(HDRip|HD[\s-]?RIP|WEBHDRIP|WEB[\s-]?HD[\s-]?RIP)/i,
  DVDRip  : /(DVD|DVDRIP|DVD[\s-]?RIP)/i,
  CAM     : /(CAM|HDTC|HDCAM|HDTS|DVDSCR|PREDVD|PRE DVD|S[\s-]?print|Pre[\s-]?DVD)/i,
  MPack   : /\b(\d{4})[\s-]+(\d{4})\b/i,
  UHD     : /(2160p|4K)/i,
  HD      : /(1080p|1080)/i,
  SD      : /(720p|720)/i,
  Ep      : /S\d+E\d+/i,
  Season  : /S\d+/i,
  Awards  : /(Awards|Award|Ceremony)/i,
  Audiobook:/(audiobook|Audio[\s-]?book)/i,
  Tutorial: /(Udemy|Talkpython|Skillshare|Domestika|Fireship|CodeWithMosh|Educative|PacktPub|O'Reilly|Dometrain|CGBoost|FrontendMasters)/i,
  Games   : /(Fitgirl|Dodi|KaOs|ElAmigos|TENOKE|FLT|RUNE|PLAZA|-GOG|SKIDROW|GOG)/i,
  GamesBAK: /\[Steam Game Backup\]|Steam Backup|Epic Games Backup|Epic Backup|Rockstar Backup|EA Backup|Origin Backup|Ubisoft Backup|Battle\.?net Backup/i,
  PS      : /(Ps[45]|PS[45]|Duplex|cusa|CUSA\d+|Psn|Playable)/i,
  Xbox360 : /(XBOX360|xbox[\s-]?360)/i,
  Crack   : /(Crack|crack[\s-]?only|Patch(?:s)?|Crackfix)/i,
  Anime   : /(Animation)/i,
};

function detectCategory(title, deco) {
  const t = title;
  if (RX.MPack.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '2';
  if (RX.WEBRip.test(t) && !RX.Ep.test(t) && !RX.Season.test(t) && !RX.Awards.test(t)) return '83';
  if (RX.WEBDL.test(t) && RX.UHD.test(t) && !RX.Ep.test(t) && !RX.Season.test(t) && !RX.Awards.test(t)) return '82';
  if (RX.WEBDL.test(t) && !RX.UHD.test(t) && !RX.Ep.test(t) && !RX.Season.test(t) && !RX.Awards.test(t)) return '55';
  if (RX.Remux.test(t) && RX.UHD.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '86';
  if (RX.Remux.test(t) && !RX.UHD.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '76';
  if (RX.BluRay.test(t) && RX.UHD.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '80';
  if (RX.BluRay.test(t) && RX.HD.test(t) && !RX.Ep.test(t) && !RX.Season.test(t) && !RX.Awards.test(t)) return '47';
  if (RX.BluRay.test(t) && RX.SD.test(t) && !RX.Ep.test(t) && !RX.Season.test(t) && !RX.Awards.test(t)) return '42';
  if (RX.BluRay.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '24';
  if (RX.HDRip.test(t) && !RX.Ep.test(t) && !RX.Season.test(t) && !RX.Awards.test(t)) return '46';
  if (RX.CAM.test(t) && !RX.Tutorial.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '4';
  if (RX.DVDRip.test(t) && !RX.GamesBAK.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '1';
  if (RX.BD3D.test(t) && !RX.GamesBAK.test(t) && !RX.Ep.test(t) && !RX.Season.test(t)) return '67';
  if (RX.Ep.test(t) && !RX.GamesBAK.test(t) && RX.UHD.test(t) && !RX.Awards.test(t)) return '84';
  if (RX.Ep.test(t) && !RX.GamesBAK.test(t) && (RX.HD.test(t)||RX.SD.test(t)) && !RX.Awards.test(t)) return '61';
  if (RX.Ep.test(t) && !RX.GamesBAK.test(t) && !RX.Awards.test(t)) return '5';
  if (RX.Season.test(t) && !RX.GamesBAK.test(t) && RX.UHD.test(t) && !RX.Awards.test(t)) return '85';
  if (RX.Season.test(t) && !RX.GamesBAK.test(t) && (RX.HD.test(t)||RX.SD.test(t)) && !RX.Awards.test(t)) return '62';
  if (RX.Season.test(t) && !RX.GamesBAK.test(t) && !RX.PS.test(t) && !RX.Awards.test(t)) return '41';
  if (RX.Crack.test(t) && !RX.WEBDL.test(t) && !RX.WEBRip.test(t) && !RX.Ep.test(t) && !RX.BluRay.test(t)) return '60';
  if (RX.GamesBAK.test(t) && !RX.Tutorial.test(t) && !RX.Crack.test(t)) return '81';
  if (RX.PS.test(t) && !RX.WEBDL.test(t) && !RX.WEBRip.test(t) && !RX.Ep.test(t) && !RX.BluRay.test(t)) return '43';
  if (RX.Xbox360.test(t) && !RX.WEBDL.test(t) && !RX.WEBRip.test(t) && !RX.Ep.test(t)) return '14';
  if (RX.Games.test(t) && !RX.Crack.test(t) && !RX.WEBDL.test(t) && !RX.WEBRip.test(t) && !RX.Ep.test(t) && !RX.BluRay.test(t) && !RX.Audiobook.test(t)) return '10';
  if (RX.Tutorial.test(t) && !RX.GamesBAK.test(t) && !RX.Xbox360.test(t) && !RX.WEBDL.test(t) && !RX.WEBRip.test(t) && !RX.Ep.test(t) && !RX.BluRay.test(t) && !RX.HDRip.test(t) && !RX.Audiobook.test(t)) return '39';
  if (RX.Awards.test(t) && (RX.WEBRip.test(t)||RX.WEBDL.test(t)||RX.HDRip.test(t)||RX.BluRay.test(t))) return '66';
  if (deco) {
    const m = deco.match(/Complete name\s*:\s*([\s\S]*?)\n\s*\w/);
    if (m) {
      const cn = m[1].trim();
      if (cn.endsWith('.m4b') || cn.endsWith('.pdf') || (RX.Audiobook.test(t) && cn.endsWith('.mp3'))) return '36';
      if (cn.endsWith('.flac')) return '71';
      if (cn.endsWith('.mp3'))  return '22';
    }
  }
  return '';
}

function applyCategory() {
  if (manualCat && !titleEditing) return;
  const title = (document.querySelector('#torrent_name') || {}).value || '';
  const deco  = (document.getElementById('torr-descr')  || {}).value || '';
  const cat   = detectCategory(title, deco);
  if (!autoMode || manualCat) return;
  if (cat && cat !== lastCat) { selectCategory(cat); lastCat = cat; }
  else if (!cat && lastCat)  { selectCategory('');  lastCat = ''; }
}

function checkIMDb() {
  if (!autoMode || manualCat) return;
  const url = (document.querySelector('#imdb_url') || {}).value || '';
  if (!url) { applyCategory(); return; }
  const id = (url.match(/tt\d+/) || [])[0];
  if (!id) return;
  $.get(`https://www.omdbapi.com/?apikey=${nextOmdbKey()}&i=${id}`, data => {
    if (data.Response !== 'True') return;
    if (data.Genre && data.Genre.includes('Documentary') && lastCat !== '9') {
      selectCategory('9'); lastCat = '9'; isDocumentary = true;
    } else if (data.Genre && data.Genre.includes('Animation') && data.Language && /\bJapanese\b/.test(data.Language) && lastCat !== '28') {
      selectCategory('28'); lastCat = '28'; isDocumentary = true;
    }
  });
}

function addAutoModeToggle() {
  const form = document.querySelector('form');
  if (!form) return;
  const wrap = document.createElement('div');
  wrap.style.cssText = 'margin-bottom:10px;display:flex;align-items:center;gap:6px;';
  const cb = document.createElement('input');
  cb.type = 'checkbox'; cb.id = 'atomicAutoMode'; cb.checked = true;
  const lbl = document.createElement('label');
  lbl.htmlFor   = 'atomicAutoMode';
  lbl.textContent = 'Auto Mode (Atomic TBD Auto Upload v5.0)';
  lbl.style.color = '#00cfff';
  wrap.append(cb, lbl);
  form.insertBefore(wrap, form.firstChild);
  cb.addEventListener('change', () => {
    autoMode = cb.checked; manualCat = false;
    if (autoMode) { applyCategory(); checkIMDb(); }
  });
}

function initCategoryWatcher() {
  addAutoModeToggle();
  applyCategory();
  checkIMDb();

  const titleField = document.querySelector('#torrent_name');
  if (titleField) {
    titleField.addEventListener('input', () => {
      titleEditing = true; applyCategory(); titleEditing = false;
      autoMode = true; manualCat = false; isDocumentary = false;
    });
  }
  const imdbField = document.querySelector('#imdb_url');
  if (imdbField) imdbField.addEventListener('input', checkIMDb);

  categoryPollId = setInterval(() => {
    if (!isDocumentary && !titleEditing) { applyCategory(); checkIMDb(); }
  }, 1000);
}

window.addEventListener('beforeunload', () => { if (categoryPollId) clearInterval(categoryPollId); });
initCategoryWatcher();

// =========================================================
// 🔍 IMDb SEARCH & AUTO-FILL
// =========================================================

function removeExtensions(name) {
  for (const ext of extensionsToRemove)
    if (name.toLowerCase().endsWith(ext)) return name.slice(0, -ext.length);
  return name;
}

function cleanTitle(raw) {
  return raw
    .replace(/[().]/g, ' ')
    .replace(/(\sS\d+E\d+|\sS\d+|\sE\d+).*$/i, ' ')
    .replace(/\d{3,4}p|UHD|HD|SD|Hindi.*/gi, ' ')
    .trim();
}

function extractTitleAndYear(input) {
  const sanitised = input.replace(/[().]/g, ' ');
  const m = sanitised.match(/(.*?)\s(\d{4})/);
  if (m) {
    const yr = parseInt(m[2]);
    if (yr >= 1200 && yr <= new Date().getFullYear())
      return { title: cleanTitle(m[1].trim()), year: m[2] };
  }
  return { title: cleanTitle(input), year: null };
}

function searchIMDb(title, year, cb) {
  let url = `https://www.omdbapi.com/?apikey=${nextOmdbKey()}&s=${encodeURIComponent(title)}`;
  if (year) url += `&y=${year}`;
  $.get(url, data => {
    if (data.Response === 'True') showIMDbPopup(data.Search, cb);
    else {
      const f = document.querySelector('#imdb_url');
      if (f) { f.value = 'Not found — try manually'; f.focus(); }
      cb(new Error(data.Error));
    }
  }).fail(() => cb(new Error('Request failed')));
}

function fillIMDb(id) {
  const f = document.querySelector('#imdb_url');
  if (f) {
    f.value = `https://www.imdb.com/title/${id}/`;
    f.focus();
    const lbl = document.querySelector('label[for="imdb_url"]');
    if (lbl) lbl.classList.add('active');
  }
}

function showIMDbPopup(results, cb) {
  document.getElementById('atomic-imdb-popup')?.remove();
  const pop = document.createElement('div');
  pop.id = 'atomic-imdb-popup';
  Object.assign(pop.style, {
    position:'fixed', top:'50%', left:'50%', transform:'translate(-50%,-50%)',
    background:'#060e1f', border:'1px solid #00cfff55', borderRadius:'12px',
    padding:'20px', zIndex:'9999', maxHeight:'380px', overflowY:'auto',
    minWidth:'320px', boxShadow:'0 0 30px rgba(0,207,255,0.15)',
  });

  const closeBtn = document.createElement('button');
  closeBtn.textContent = '✕';
  Object.assign(closeBtn.style, {
    position:'absolute', top:'8px', right:'10px', background:'#1a2a3a',
    color:'#00cfff', border:'none', borderRadius:'6px', padding:'4px 10px',
    cursor:'pointer', fontWeight:'700',
  });
  closeBtn.onclick = () => pop.remove();
  pop.appendChild(closeBtn);

  const heading = document.createElement('div');
  heading.textContent = '🔍 IMDb Results — Atomic TBD Auto Upload';
  Object.assign(heading.style, { color:'#00cfff', fontWeight:'800', marginBottom:'14px', fontFamily:'Bahnschrift,sans-serif' });
  pop.appendChild(heading);

  const notFoundImg = 'https://static.torrentbd.net/fe56b068f0cd6b6c76caec27d3b2f8ed.png';
  results.forEach(r => {
    const item = document.createElement('div');
    Object.assign(item.style, {
      display:'flex', alignItems:'center', gap:'12px', padding:'8px',
      borderRadius:'8px', cursor:'pointer', marginBottom:'8px',
      border:'1px solid transparent', transition:'all 0.15s',
    });
    item.onmouseenter = () => { item.style.background='rgba(0,207,255,0.07)'; item.style.borderColor='#00cfff33'; };
    item.onmouseleave = () => { item.style.background=''; item.style.borderColor='transparent'; };

    const img = document.createElement('img');
    img.src = (r.Poster && r.Poster !== 'N/A') ? r.Poster : notFoundImg;
    img.style.cssText = 'width:44px;height:64px;object-fit:cover;border-radius:4px;';

    const info = document.createElement('div');
    info.style.color = '#e2e8f0';
    info.innerHTML = `<strong style="color:#a78bfa">${r.Title}</strong><br><span style="color:#64748b;font-size:.82em">${r.Type} · ${r.Year}</span>`;

    item.append(img, info);
    item.addEventListener('click', () => { fillIMDb(r.imdbID); pop.remove(); cb(null, r.imdbID); });
    pop.appendChild(item);
  });

  document.body.appendChild(pop);
  setTimeout(() => {
    document.addEventListener('click', e => { if (!pop.contains(e.target)) pop.remove(); }, { once: true });
  }, 100);
}

function addIMDbButton() {
  const imdbField = document.querySelector('#imdb_url');
  const nameField = document.querySelector('#torrent_name');
  if (!imdbField || !nameField) return;

  const wrap = document.createElement('div');
  wrap.style.cssText = 'display:flex;align-items:center;gap:0;';

  const btn = document.createElement('button');
  btn.textContent = '🔍 Get IMDb';
  Object.assign(btn.style, {
    background:'linear-gradient(135deg,#7c3aed,#4f46e5)',
    color:'#fff', border:'none', padding:'10px 16px',
    borderRadius:'8px 0 0 8px', fontWeight:'700', cursor:'pointer',
    whiteSpace:'nowrap', fontFamily:'Bahnschrift,sans-serif',
  });

  imdbField.style.borderRadius = '0 8px 8px 0';
  imdbField.parentNode.insertBefore(wrap, imdbField);
  wrap.append(btn, imdbField);

  const lbl = document.querySelector('label[for="imdb_url"]');
  if (lbl) lbl.style.paddingLeft = '120px';

  btn.addEventListener('click', e => {
    e.preventDefault();
    const info = extractTitleAndYear(nameField.value);
    searchIMDb(info.title, info.year, () => {});
  });
}

function addClearIcon(field, id) {
  const container = field?.parentNode;
  if (!container || document.getElementById(id)) return;
  if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
  const ic = document.createElement('span');
  ic.id = id; ic.textContent = '✖';
  Object.assign(ic.style, {
    cursor:'pointer', fontSize:'1.1em', color:'#ef4444',
    position:'absolute', right:'10px', top:'10px', zIndex:'10',
  });
  ic.onclick = () => { field.value = ''; field.focus(); };
  container.appendChild(ic);
}

function addFileListener() {
  const fi   = document.querySelector('input[type=file][name=torrent]');
  const name = document.getElementById('torrent_name');
  const imdb = document.querySelector('#imdb_url');
  if (!fi) return;
  fi.addEventListener('change', () => {
    const f = fi.files[0];
    if (!f || !f.name.endsWith('.torrent')) return;
    const base = removeExtensions(f.name.replace('.torrent', ''));
    if (!name.value.trim()) name.value = base.trim();
    if (!imdb.value.trim()) {
      const info = extractTitleAndYear(name.value);
      searchIMDb(info.title, info.year, (err, id) => { if (!err) fillIMDb(id); });
    }
  });
}

window.addEventListener('load', () => {
  addIMDbButton();
  addFileListener();
  addClearIcon(document.getElementById('torrent_name'), 'atomic-clear-name');
  addClearIcon(document.querySelector('#imdb_url'), 'atomic-clear-imdb');

  const observer = new MutationObserver(() => {
    document.querySelectorAll('.swal2-popup').forEach(p => {
      const t = p.querySelector('#swal2-title');
      if (t && t.textContent.includes('Essential details missing'))
        p.querySelector('.swal2-confirm')?.click();
    });
  });
  observer.observe(document.body, { childList: true, subtree: true });
});

// =========================================================
// 🎮 GAME DESCRIPTION — Steam
// =========================================================

const internalYouTubeKeys = [
  'AIzaSyDSqZbdWwU8ufssrFRlgUIoy625IprArhU',
  'AIzaSyCqZcGyWKFX3li4GRQ4bidrsIzjG52VzTY',
];
const ytKeys = youtubeApiKeys.length ? youtubeApiKeys.concat(internalYouTubeKeys) : internalYouTubeKeys;

const steamBtn = document.createElement('button');
steamBtn.classList.add('btn','darken-2');
steamBtn.innerHTML = '<i class="material-icons right">games</i>GAME DESC';
steamBtn.style.cssText = 'margin-right:1px;height:32px;width:175px;';
steamBtn.type = 'button';

function refreshGameBtnVisibility() {
  const sel = document.querySelector('select[name="type"]');
  const cat = sel ? parseInt(sel.value) : null;
  const valid = [10,60,52,81];
  if (valid.includes(cat)) {
    if (!steamBtn.parentNode) {
      document.querySelector('.bbc-more-contents')?.parentNode.insertBefore(
        steamBtn, document.querySelector('.bbc-more-contents')?.nextSibling
      );
    }
  } else if (steamBtn.parentNode) {
    steamBtn.parentNode.removeChild(steamBtn);
  }
}

document.querySelector('select[name="type"]')?.addEventListener('change', refreshGameBtnVisibility);
refreshGameBtnVisibility();

steamBtn.addEventListener('click', () => {
  const filter = document.querySelector('#filter');
  const nameEl = document.querySelector('#torrent_name');
  const cat    = filter ? filter.value : '';
  const title  = nameEl ? nameEl.value : '';

  if (!['10','52','60','81'].includes(cat)) { alert('Please select a valid Game category.'); return; }

  if (document.querySelector('select[name="lang"]')?.value === '0')
    document.querySelector('select[name="lang"]').value = '1';

  let clean = title;
  const cleanPatterns = [
    /SEASON\s.*$/, /Chapter\s.*$/, /Episode.*$/, /v\d+(\.\d+)?\s.*$/, /v\d+.*$/,
    /\/.*$/, /\s*\[.*?\]$/, /\s*\(.*?\)/, /-.+$/, /Build \d+.*$/, /:.*$/, /update.*$/i, /\+.*$/, /\[.*$/,
  ];
  let prev;
  do {
    prev = clean;
    cleanPatterns.forEach(p => clean = clean.replace(p,''));
    clean = clean.trim();
  } while (clean !== prev);

  const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(clean + ' Steam link')}`;
  showProcessingPopup([
    '🔎 Searching Google for Steam page',
    '🎮 Fetching Steam game details',
    '⚠️  Checking mature content',
    '💻 Extracting system requirements',
    '🖼  Getting Steam screenshots',
    '🎬 Finding YouTube trailer',
    '📝 Building description BBCode',
  ]);
  advanceStep('🔎 Searching Google for Steam page');

  GM_xmlhttpRequest({
    method: 'GET', url: googleUrl,
    onload(res) {
      if (res.status !== 200 || res.responseText.includes('captcha')) {
        alert('Google blocked the request.'); window.open(googleUrl,'_blank'); hideProcessingPopup(); return;
      }
      const steamMatch = res.responseText.match(/https:\/\/store\.steampowered\.com\/app\/(\d+)/);
      if (!steamMatch) { alert('Steam page not found in results.'); hideProcessingPopup(); return; }
      fetchSteamDetails(steamMatch[1], cat, title);
    },
    onerror() { alert('Google search failed.'); hideProcessingPopup(); },
  });
});

function fetchSteamDetails(appId, cat, torrentTitle) {
  advanceStep('🎮 Fetching Steam game details');
  GM_xmlhttpRequest({
    method: 'GET',
    url: `https://store.steampowered.com/api/appdetails?appids=${appId}`,
    onload(res) {
      if (res.status !== 200) { alert('Steam API failed.'); hideProcessingPopup(); return; }
      const d = JSON.parse(res.responseText)[appId];
      if (!d.success) { alert('Steam returned no data.'); hideProcessingPopup(); return; }

      const desc   = convertHTMLToBBCode(d.data.detailed_description);
      const header = cleanImageUrl(d.data.header_image);

      Promise.all([
        new Promise(r => { advanceStep('⚠️  Checking mature content');       extractMature(appId, r); }),
        new Promise(r => { advanceStep('💻 Extracting system requirements'); extractSysReq(appId, r); }),
        new Promise(r => { advanceStep('🖼  Getting Steam screenshots');      extractSteamImages(appId, r); }),
        new Promise(r => { advanceStep('🎬 Finding YouTube trailer');        extractYouTubeTrailer(appId, r); }),
        new Promise(r => { advanceStep('📝 Building description BBCode');    buildInstallSteps(appId, cat, torrentTitle, r); }),
      ]).then(([mature, sysreq, imgs, trailer, install]) => {
        let bb = '';
        bb += `[center][img]${header}[/img][/center]\n`;
        bb += [
          '[hr]','[center]',
          '[color=#ff9f43][font=Orbitron]▣ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▣[/font][/color]',
          '[img]https://i.ibb.co.com/hFmhk125/cooltext507373094567207.png[/img]',
          '[color=#ff9f43][font=Orbitron]▣ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▣[/font][/color]',
          '[/center]','[hr]',
        ].join('\n') + '\n';
        bb += `[font=Inconsolata][color=#e2e8f0][size=3][b]${desc}[/b][/size][/color][/font]\n`;

        if (mature && mature !== 'No mature content description available.') {
          bb += [
            '[hr]','[center]',
            '[color=#C00014][font=Orbitron]⚠ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚠[/font][/color]',
            '[img]https://i.ibb.co.com/0VtySkFT/cooltext507373125791937.png[/img]',
            '[color=#C00014][font=Orbitron]⚠ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚠[/font][/color]',
            '[/center]','[hr]',
          ].join('\n') + '\n';
          bb += `[font=Inconsolata][color=#ff4fa3][size=3][b]${mature}[/b][/size][/color][/font]\n`;
        }

        const sysFixed = sysreq.replace(/Recommended:/gi, [
          '\n[/b][/size][/color][/font]',
          '[hr]','[center]',
          '[color=#55efc4][font=Orbitron]⊕ ── Recommended ── ⊕[/font][/color]',
          '[/center]',
          '[font=Inconsolata][color=#55efc4][size=3][b]',
        ].join('\n'));

        bb += [
          '[hr]','[center]',
          '[color=#C00014][font=Orbitron]◉ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◉[/font][/color]',
          '[img]https://i.ibb.co.com/RGJphKtw/cooltext507373160593929.png[/img]',
          '[color=#C00014][font=Orbitron]◉ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◉[/font][/color]',
          '[/center]','[hr]',
        ].join('\n') + '\n';
        bb += ['[center]','[color=#dfe6e9][font=Orbitron]── Minimum ──[/font][/color]','[/center]'].join('\n') + '\n';
        bb += `[font=Inconsolata][color=#dfe6e9][size=3][b]${sysFixed}[/b][/size][/color][/font]\n`;

        bb += [
          '[hr]','[center]',
          '[color=#C00014][font=Orbitron]✦ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✦[/font][/color]',
          '[img]https://i.ibb.co.com/wFL8pcJp/cooltext507373188105459.png[/img]',
          '[color=#C00014][font=Orbitron]✦ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✦[/font][/color]',
          '[/center]','[hr]',
        ].join('\n') + '\n';
        bb += `[font=Inconsolata][color=#55efc4][size=3][b]${install}[/b][/size][/color][/font]\n\n`;

        bb += [
          '[hr]','[center]',
          '[color=#C00014][font=Orbitron]◈ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◈[/font][/color]',
          '[img]https://i.ibb.co.com/wZfk3R99/cooltext507373214319597.png[/img]',
          '[color=#C00014][font=Orbitron]◈ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ◈[/font][/color]',
          '[/center]','[hr]',
        ].join('\n') + `\n${imgs}\n`;

        if (trailer && trailer.trim()) {
          bb += [
            '[hr]','[center]',
            '[color=#C00014][font=Orbitron]▶ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▶[/font][/color]',
            '[img]https://i.ibb.co.com/35LF16C4/cooltext507373240190184.png[/img]',
            '[color=#C00014][font=Orbitron]▶ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▶[/font][/color]',
            '[/center]','[hr]',
          ].join('\n') + `\n${trailer}\n`;
        }

        bb += '\n' + gameThankYouBB;

        const ta = document.querySelector('#torr-descr');
        if (ta) ta.value = bb;
        setTimeout(hideProcessingPopup, 600);
      });
    },
    onerror() { alert('Steam API request failed.'); hideProcessingPopup(); },
  });
}

function cleanImageUrl(url) {
  const c = url.split('?')[0];
  return c.includes('discord') ? '' : c;
}

function convertHTMLToBBCode(html) {
  return html
    .replace(/<img\s+[^>]*src="([^"]+)"[^>]*>/gi, (_, s) => `\n\n[img]${s.split('?')[0]}[/img]\n\n`)
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<i>(.*?)<\/i>/gi, '[i]$1[/i]')
    .replace(/<b>(.*?)<\/b>/gi, '[b]$1[/b]')
    .replace(/<u>(.*?)<\/u>/gi, '[u]$1[/u]')
    .replace(/<code>(.*?)<\/code>/gi, '[code]$1[/code]')
    .replace(/<pre>(.*?)<\/pre>/gis, '[code]$1[/code]')
    .replace(/<a\s+href="([^"]+)"[^>]*>(.*?)<\/a>/gi, '[url=$1]$2[/url]')
    .replace(/<h\d>(.*?)<\/h\d>/gi, '')
    .replace(/<\/?[^>]+(>|)/g, '')
    .replace(/https:\/\/store\.steampowered\.com\/app\/\d+\/[^ ]*/g, '')
    .replace(/\[url=[^\]]*\].*?\[\/url\]/gi, '')
    .trim();
}

function extractMature(appId, cb) {
  GM_xmlhttpRequest({
    method: 'GET', url: `https://store.steampowered.com/app/${appId}`,
    onload(res) {
      if (res.status !== 200) { cb('No mature content description available.'); return; }
      const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
      const div = doc.querySelector('#game_area_content_descriptors.game_area_description');
      if (!div) { cb('No mature content description available.'); return; }
      const ps = div.querySelectorAll('p');
      cb(ps.length > 1
        ? `${ps[0].textContent.trim()}\n\n${ps[1].textContent.trim()}`
        : 'No mature content description available.');
    },
    onerror() { cb('No mature content description available.'); },
  });
}

function extractSysReq(appId, cb, retries = 3) {
  GM_xmlhttpRequest({
    method: 'GET', url: `https://store.steampowered.com/app/${appId}`,
    onload(res) {
      if (res.status !== 200) {
        if (retries > 0) setTimeout(() => extractSysReq(appId, cb, retries - 1), 2000);
        else cb('N/A'); return;
      }
      const doc  = new DOMParser().parseFromString(res.responseText, 'text/html');
      const sect = doc.querySelector('.game_page_autocollapse.sys_req');
      if (!sect) { cb('N/A'); return; }
      const win = sect.querySelector('.game_area_sys_req[data-os="win"]') || sect.querySelector('.game_area_sys_req');
      if (!win) { cb('N/A'); return; }
      const clone = win.cloneNode(true);
      clone.querySelector('.game_area_sys_req_note')?.remove();
      const text = clone.innerHTML
        .replace(/<br\s*\/?>/gi,'\n').replace(/<li>/gi,'• ').replace(/<\/li>/gi,'')
        .replace(/<ul>/gi,'').replace(/<\/ul>/gi,'').replace(/<strong>(.*?)<\/strong>/gi,'$1')
        .replace(/<\/?[^>]+(>|)/g,'').replace(/Minimum:/gi,'').trim();
      cb(text);
    },
    onerror() {
      if (retries > 0) setTimeout(() => extractSysReq(appId, cb, retries - 1), 2000);
      else cb('N/A');
    },
  });
}

function extractSteamImages(appId, cb, retries = 3) {
  GM_xmlhttpRequest({
    method: 'GET', url: `https://store.steampowered.com/app/${appId}`,
    onload(res) {
      if (res.status !== 200) {
        if (retries > 0) setTimeout(() => extractSteamImages(appId, cb, retries - 1), 2000);
        else cb(''); return;
      }
      const doc  = new DOMParser().parseFromString(res.responseText, 'text/html');
      const urls = Array.from(doc.querySelectorAll('a.highlight_screenshot_link'))
        .map(a => {
          const href  = a.getAttribute('href') || '';
          const match = href.match(/https:\/\/cdn\.akamai\.steamstatic\.com[^?]+\.jpg/);
          return match ? match[0] : null;
        }).filter(Boolean);
      cb(urls.map(u => `[center][img]${u}[/img][/center]`).join('\n'));
    },
    onerror() {
      if (retries > 0) setTimeout(() => extractSteamImages(appId, cb, retries - 1), 2000);
      else cb('');
    },
  });
}

function extractYouTubeTrailer(appId, cb) {
  GM_xmlhttpRequest({
    method: 'GET', url: `https://store.steampowered.com/app/${appId}`,
    onload(res) {
      if (res.status !== 200) { cb(''); return; }
      const doc  = new DOMParser().parseFromString(res.responseText, 'text/html');
      const name = (doc.querySelector('#appHubAppName') || {}).textContent?.trim() || '';
      if (!name) { cb(''); return; }
      searchYouTube(name, 0, cb);
    },
    onerror() { cb(''); },
  });
}

function searchYouTube(gameName, idx, cb) {
  if (idx >= ytKeys.length) { cb(''); return; }
  GM_xmlhttpRequest({
    method: 'GET',
    url: `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q=${encodeURIComponent(gameName + ' game trailer')}&key=${ytKeys[idx]}&maxResults=1`,
    onload(r) {
      if (r.status !== 200) { searchYouTube(gameName, idx + 1, cb); return; }
      const d = JSON.parse(r.responseText);
      if (d.items && d.items.length) {
        const vid = d.items[0].id.videoId;
        cb(`[center]\n[video=https://www.youtube.com/watch?v=${vid}]\n[/center]`);
      } else { searchYouTube(gameName, idx + 1, cb); }
    },
    onerror() { searchYouTube(gameName, idx + 1, cb); },
  });
}

function buildInstallSteps(appId, cat, torrentTitle, cb) {
  const stepsLib = {
    1:  ['• Run the installer as administrator','• Press the up arrow on your keyboard','• Click Install → Continue','• Select installation destination','• Click Next → Install'],
    2:  ['➩ Run Verify BIN files before installation (optional)','➩ Run setup.exe','➩ Follow on-screen instructions','➩ Play and enjoy!'],
    3:  ['➠ Run setup.exe or install.exe','➠ Follow on-screen instructions','➠ Play and enjoy!'],
    4:  ['➵ Burn or Mount the .iso','➵ Run setup.exe','➵ Follow on-screen instructions','➵ Enjoy!'],
    5:  ['◈ Burn or mount the .iso','◈ Install using the installer','◈ Copy "Crack" to game folder if needed','◈ Play the game (block it in your firewall)'],
    6:  ['❏ Install / Copy everything into the game folder','❏ Play the game (block it in your firewall)'],
    7:  ['➥ ','➥ ','➥ ','➥ '],
    8:  ['➥ Launch Steam','➥ Click Steam → Restore Game Backup','➥ Browse to the downloaded backup folder','➥ Click Next and wait for restoration','➥ Ready to Play!'],
    9:  ['➥ Open Epic Games Launcher','➥ Right-click the game → Install, pick the directory','➥ Let it download 4–5 MiB, then pause & close the Launcher','➥ Delete the newly created folder at the install path','➥ Paste the downloaded game folder there','➥ Resume in Epic — it will verify and finish'],
    10: ['➵ Open Rockstar Games Launcher and sign in','➵ Select Install Now and choose where you downloaded the game','➵ Wait for verification to complete'],
    11: ['• Origin: Right-click the game → Locate Game → pick download folder','• EA App: Select the game → choose download folder as install dir → auto-loads'],
    12: ['➥ Open Ubisoft Connect','➥ Tap Locate Installed Game → pick download folder','➥ Files will be detected and verified','➥ Play directly from Ubisoft Connect'],
    13: ['➥ Open Battle.net → Locate the Game → select download folder','➥ The app discovers existing files and installs','• If wrong-version error: Blizzard logo → Settings → Downloads → Scan for Games → Update'],
  };

  const kw = {
    1:  ['DODI','dodi'],
    2:  ['FitGirl','fitgirl','Fitgirl'],
    3:  ['KaOs','kaos'],
    4:  ['ElAmigos','elamigos'],
    8:  ['Steam Backup','Steam-Backup','[Steam Backup]'],
    9:  ['Epic Backup','Epic Games Backup','[Epic Backup]'],
    10: ['Rockstar Backup','Rockstar Games Backup'],
    11: ['EA Backup','Origin Backup','EA/Origin Backup','EA APP Backup'],
    12: ['Ubisoft Backup','Ubisoft Connect Backup'],
    13: ['Battle.net Backup','Battle net Backup','Battle Blizzard Backup'],
  };

  let opt = null;
  const t = torrentTitle;

  if (cat === '60' || cat === '52') { opt = 6; }
  else if (cat === '81') {
    for (const key of [8,9,10,11,12,13]) {
      if (kw[key] && kw[key].some(k => t.toLowerCase().includes(k.toLowerCase()))) { opt = key; break; }
    }
    if (!opt) opt = 7;
  } else if (cat === '10') {
    for (const key of [1,2,3,4]) {
      if (kw[key] && kw[key].some(k => t.toLowerCase().includes(k.toLowerCase()))) { opt = key; break; }
    }
    if (!opt) opt = 5;
  } else { opt = 5; }

  cb((stepsLib[opt] || stepsLib[5]).join('\n'));
}

// =========================================================
// 🎓 TUTORIAL DESCRIPTIONS (Udemy / TalkPython / Skillshare)
// =========================================================

function addButtons() {
  const container = document.querySelector('.bbc-btn-container');
  if (!container) return;

  const mkBtn = (id, label, icon) => {
    const b = document.createElement('button');
    b.id = id; b.className = 'btn darken-2';
    b.style.cssText = 'height:32px;width:195px;margin-right:5px;display:none;';
    b.type = 'button';
    b.innerHTML = `<i class="material-icons right">${icon}</i>${label}`;
    return b;
  };

  const udemyBtn      = mkBtn('atomic-udemy',      'UDEMY',     'school');
  const talkpythonBtn = mkBtn('atomic-talkpython', 'TalkPython','school');
  const skillshareBtn = mkBtn('atomic-skillshare', 'Skillshare','school');

  container.insertBefore(udemyBtn,      container.firstChild);
  container.insertBefore(talkpythonBtn, container.firstChild);
  container.insertBefore(skillshareBtn, container.firstChild);

  udemyBtn.addEventListener('click',      fetchUdemy);
  talkpythonBtn.addEventListener('click', fetchTalkPython);
  skillshareBtn.addEventListener('click', fetchSkillshare);

  function refresh() {
    const cat   = (document.querySelector('#filter') || {}).value || '';
    const title = ((document.querySelector('#torrent_name') || {}).value || '').toLowerCase();
    udemyBtn.style.display      = (cat==='39' && title.includes('udemy'))      ? 'inline-block':'none';
    talkpythonBtn.style.display = (cat==='39' && title.includes('talkpython')) ? 'inline-block':'none';
    skillshareBtn.style.display = (cat==='39' && title.includes('skillshare')) ? 'inline-block':'none';
    if (cat === '39') {
      const l = document.querySelector('select[name="lang"]');
      if (l && (l.value === '0' || l.options[l.selectedIndex]?.text === 'Choose Language')) l.value = '1';
    }
  }

  document.querySelector('#filter')?.addEventListener('change', refresh);
  document.querySelector('#torrent_name')?.addEventListener('input', refresh);
  refresh();
}

function htmlToBBCode(html) {
  return html
    .replace(/<strong>(.*?)<\/strong>/g,'[b]$1[/b]')
    .replace(/<p><strong>(.*?)<\/strong><\/p>/g,'[b]$1[/b]\n')
    .replace(/<li><p>(.*?)<\/p><\/li>/g,'• $1\n')
    .replace(/<li><strong>(.*?)<\/strong><\/li>/g,'[b]$1[/b]')
    .replace(/<li>(.*?)<\/li>/g,'• $1')
    .replace(/<\/ul><p>/g,'\n')
    .replace(/<p>(.*?)<\/p>/g,'$1\n\n')
    .replace(/<p>/g,'\n')
    .replace(/<\/ul[^>]*>/g,'\n')
    .replace(/<ul[^>]*>/g,'\n')
    .replace(/<b>(.*?)<\/b>/g,'[b]$1[/b]')
    .replace(/<i>(.*?)<\/i>/g,'[i]$1[/i]')
    .replace(/<u>(.*?)<\/u>/g,'[u]$1[/u]')
    .replace(/<code>(.*?)<\/code>/g,'[code]$1[/code]')
    .replace(/<pre>(.*?)<\/pre>/gis,'[code]$1[/code]')
    .replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/g,'[url=$1]$2[/url]')
    .replace(/<\/?(?:div|span|br)[^>]*>/g,'')
    .replace(/&amp;/g,'&').replace(/&nbsp;/g,' ')
    .replace(/<[^>]+>/g,'')
    .trim();
}

function tutorialHeader(icon, label, color, borderChar) {
  return [
    '[hr]','[center]',
    `[color=${color}][font=Orbitron]${borderChar} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ${borderChar}[/font][/color]`,
    `[size=4][font=Russo One][color=${color}]${icon} ${label}[/color][/font][/size]`,
    `[color=${color}][font=Orbitron]${borderChar} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ${borderChar}[/font][/color]`,
    '[/center]','[hr]',
  ].join('\n') + '\n';
}

function insertDescription(bbcode) {
  const ta = document.querySelector('#torr-descr');
  if (ta) ta.value = '\n\n' + bbcode + ta.value;
  else alert('Description field not found.');
  hideProcessingPopup();
}

function googleSearchFirst(query, urlTest, cb) {
  showProcessingPopup(['🔍 Searching Google','📥 Fetching course page']);
  advanceStep('🔍 Searching Google');
  GM_xmlhttpRequest({
    method: 'GET', url: `https://www.google.com/search?q=${encodeURIComponent(query)}`,
    onload(r) {
      const doc  = new DOMParser().parseFromString(r.responseText, 'text/html');
      const link = Array.from(doc.querySelectorAll('a')).find(a => urlTest(a.href));
      if (link) { advanceStep('📥 Fetching course page'); cb(link.href); }
      else { alert('Course page not found via Google.'); hideProcessingPopup(); }
    },
    onerror() { alert('Google search failed.'); hideProcessingPopup(); },
  });
}

function fetchUdemy() {
  const t = (document.querySelector('#torrent_name') || {}).value?.trim() || '';
  if (!t) { alert('Title is empty.'); return; }
  if (!/udemy/i.test(t)) { alert("Add 'Udemy' to the torrent title first."); return; }
  const q = t.replace(/^Udemy\s*[-—]\s*/i,'').replace(/\s*\[.*?\]/,'').trim();
  googleSearchFirst(q + ' Udemy course', h => h.includes('udemy.com/course'), url => {
    GM_xmlhttpRequest({
      method:'GET', url,
      onload(r) {
        const d = new DOMParser().parseFromString(r.responseText,'text/html');
        let bb = `[size=3][font=Russo One][color=#38bdf8][center][url=${url}]→ View Course on Udemy ↗[/url][/center][/color][/font][/size]\n`;
        const desc  = d.querySelector('div[data-purpose="safely-set-inner-html:description:description"]');
        const req   = d.querySelector('[data-purpose="requirements-title"] + ul');
        const learn = d.querySelector('.what-you-will-learn--objectives-list--qsvE2');
        const who   = d.querySelector('ul.styles--audience__list----YbP');
        if (desc)  bb += tutorialHeader('📖','D E S C R I P T I O N','#38bdf8','◆')      + `[font=Inconsolata][color=#e2e8f0]${htmlToBBCode(desc.innerHTML)}[/color][/font]`;
        if (req)   bb += tutorialHeader('📋','R E Q U I R E M E N T S','#a78bfa','◈')    + `[font=Inconsolata][color=#a78bfa]${htmlToBBCode(req.innerHTML)}[/color][/font]`;
        if (learn) bb += tutorialHeader('🎯','W H A T  Y O U  W I L L  L E A R N','#34d399','▸') + `[font=Inconsolata][color=#34d399]${htmlToBBCode(learn.innerHTML)}[/color][/font]`;
        if (who)   bb += tutorialHeader('👤','W H O  I S  T H I S  F O R','#fbbf24','★') + `[font=Inconsolata][color=#fbbf24]${htmlToBBCode(who.innerHTML)}[/color][/font]`;
        bb += '\n' + tutorialThankYouBB;
        insertDescription(bb);
      },
      onerror() { alert('Failed to fetch Udemy page.'); hideProcessingPopup(); },
    });
  });
}

function fetchTalkPython() {
  const t = (document.querySelector('#torrent_name') || {}).value?.trim() || '';
  if (!t) { alert('Title is empty.'); return; }
  if (!/talkpython/i.test(t)) { alert("Add 'TalkPython' to the torrent title first."); return; }
  const q = t.replace(/^TalkPython\s*[-—]\s*/i,'').replace(/\s*\[.*?\]/,'').trim();
  googleSearchFirst(q + ' TalkPython course', h => h.includes('training.talkpython.fm/courses'), url => {
    GM_xmlhttpRequest({
      method:'GET', url,
      onload(r) {
        const d = new DOMParser().parseFromString(r.responseText,'text/html');
        let learn = '', who = '';
        d.querySelectorAll('h2').forEach(h2 => {
          const txt = h2.innerText?.trim() || '';
          let section = '';
          if (/Who is this course for/i.test(txt)) section = 'who';
          else if (/What will you learn|What topics are covered/i.test(txt)) section = 'learn';
          let sib = h2.nextElementSibling;
          while (sib && sib.tagName !== 'H2') {
            if (/^(P|UL)/.test(sib.tagName)) {
              if (section==='who')   who   += sib.outerHTML;
              else if (section==='learn') learn += sib.outerHTML;
            }
            sib = sib.nextElementSibling;
          }
        });
        learn = learn.replace(/<li><p>View the full <a[^>]*>course outline<\/a>\.<\/p><\/li>/gi,'');
        const descEl = d.querySelector('.description');
        let bb = `[size=3][font=Russo One][color=#38bdf8][center][url=${url}]→ View Course on TalkPython ↗[/url][/center][/color][/font][/size]\n`;
        if (descEl) {
          let dh = descEl.innerHTML.replace(/<h2>Course Summary<\/h2>/gi,'');
          bb += tutorialHeader('📖','D E S C R I P T I O N','#38bdf8','◆') + `[font=Inconsolata][color=#e2e8f0]${htmlToBBCode(dh)}[/color][/font]`;
        }
        if (learn) bb += tutorialHeader('🎯','W H A T  Y O U  W I L L  L E A R N','#34d399','▸') + `[font=Inconsolata][color=#34d399]${htmlToBBCode(learn)}[/color][/font]`;
        if (who)   bb += tutorialHeader('👤','W H O  I S  T H I S  F O R','#fbbf24','★')          + `[font=Inconsolata][color=#fbbf24]${htmlToBBCode(who)}[/color][/font]`;
        bb += '\n' + tutorialThankYouBB;
        insertDescription(bb);
      },
      onerror() { alert('Failed to fetch TalkPython page.'); hideProcessingPopup(); },
    });
  });
}

function fetchSkillshare() {
  const t = (document.querySelector('#torrent_name') || {}).value?.trim() || '';
  if (!t) { alert('Title is empty.'); return; }
  if (!/skillshare/i.test(t)) { alert("Add 'Skillshare' to the torrent title first."); return; }
  const q = t.replace(/^['"']?Skillshare['"]?\s*[-—]\s*/i,'').replace(/\s*\[.*?\]/,'').trim();
  googleSearchFirst(q + ' Skillshare course', h => h.includes('skillshare.com'), url => {
    GM_xmlhttpRequest({
      method:'GET', url,
      onload(r) {
        const d       = new DOMParser().parseFromString(r.responseText,'text/html');
        const descCol = d.querySelector('.description-column .rich-content-wrapper');
        let dh = descCol ? descCol.innerHTML : '';
        dh = dh.split('\n').filter(l => !l.includes('----------')).join('\n');
        dh = dh.replace(/\[b\]\[\/b\]/gi,'');
        ['Other Useful Links:','© moonlearning.io'].forEach(s => {
          const i = dh.indexOf(s); if (i !== -1) dh = dh.substring(0, i);
        });
        dh = dh.replace(/Course Summary/g,'');
        let bb = `[size=3][font=Russo One][color=#38bdf8][center][url=${url}]→ View Course on Skillshare ↗[/url][/center][/color][/font][/size]\n`;
        if (dh) bb += tutorialHeader('📖','D E S C R I P T I O N','#38bdf8','◆') + `[font=Inconsolata][color=#e2e8f0]${htmlToBBCode(dh)}[/color][/font]`;
        bb += '\n' + tutorialThankYouBB;
        insertDescription(bb);
      },
      onerror() { alert('Failed to fetch Skillshare page.'); hideProcessingPopup(); },
    });
  });
}

addButtons();

})();