Atomic TBD Auto Upload

Auto upload tool (Fixed for Violentmonkey)

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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

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

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

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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

})();