Auto upload tool (Fixed for Violentmonkey)
// ==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(/&/g,'&').replace(/ /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();
})();