Scan torrents, exclude keywords, auto-open. Auto-opens ONLY the last sample image. Auto-rejects lower resolution duplicates. 4s Delay.
// ==UserScript==
// @name EXT Torrent Scanner (v3.3)
// @namespace
// @version 3.3
// @description Scan torrents, exclude keywords, auto-open. Auto-opens ONLY the last sample image. Auto-rejects lower resolution duplicates. 4s Delay.
// @author You
// @license MIT
// @match
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const CONFIG = {
scanDelay: 4000, // 4 seconds wait time
version: "3.3"
};
// Allowed image domains for detail pages
const IMAGE_DOMAINS = [
'trafficimage.club',
'14xpics.space',
'imagetwist.com',
'imgtraffic.com' // Added new domain
];
// --- STATE MANAGEMENT ---
let state = {
isScanning: false,
scanQueue: [],
queueIndex: 0,
stats: { opened: 0, excluded: 0, total: 0 }
};
let selectedTextBuffer = "";
// --- STORAGE MANAGEMENT ---
const DEFAULTS = {
excludeWords: [],
lastRunTime: null,
useLastRunTime: false,
autoImages: true
};
let settings = GM_getValue('ext_scanner_data', DEFAULTS);
function saveSettings() {
GM_setValue('ext_scanner_data', settings);
}
if (typeof GM_addValueChangeListener === 'function') {
GM_addValueChangeListener('ext_scanner_data', function(name, oldVal, newVal, remote) {
if (remote) {
settings = newVal;
if (document.getElementById('ext-scanner-overlay')) {
updateUI();
}
}
});
}
// --- HELPER: RESOLUTION PARSING ---
function extractResolution(title) {
title = title.toLowerCase();
if (title.includes('2160p') || title.includes('4k')) return 2160;
if (title.includes('1080p')) return 1080;
if (title.includes('720p')) return 720;
if (title.includes('480p')) return 480;
return 0; // Unknown/Low
}
function normalizeTitle(title) {
return title.toLowerCase()
.replace(/2160p|4k|1080p|720p|480p/g, '')
.replace(/mp4|mkv|wrb|nbq|-xc|-nbq/g, '')
.replace(/[^a-z0-9]/g, '');
}
// --- DOM & UI HELPERS ---
function createOverlay() {
if (document.getElementById('ext-scanner-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'ext-scanner-overlay';
overlay.style.cssText = `
position: fixed; bottom: 20px; right: 20px; width: 300px;
background: #1e1e1e; border: 1px solid #333; border-radius: 8px;
padding: 15px; z-index: 9999; color: #eee; font-family: sans-serif;
box-shadow: 0 10px 30px rgba(0,0,0,0.5); font-size: 13px;
`;
overlay.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<span style="font-weight:bold; color:#4db8ff;">🔍 EXT Scanner (v${CONFIG.version})</span>
<span id="scan-status" style="font-size:11px; color:#888;">Ready</span>
</div>
<div style="display:flex; gap:5px; margin-bottom:10px;">
<input type="text" id="ext-exclude-input" placeholder="Block word..." style="flex:1; background:#2d2d2d; border:1px solid #444; color:#fff; padding:5px; border-radius:4px;">
<button id="ext-add-btn" style="background:#444; border:none; color:#fff; padding:5px 10px; border-radius:4px; cursor:pointer;">+</button>
</div>
<div id="ext-exclude-list" style="max-height:100px; overflow-y:auto; margin-bottom:10px; background:#252525; padding:5px; border-radius:4px; display:flex; flex-wrap:wrap; gap:4px;"></div>
<div style="margin-bottom:10px; border-top: 1px solid #333; padding-top: 10px;">
<label style="cursor:pointer; display:flex; align-items:center; font-size:12px; margin-bottom: 5px;">
<input type="checkbox" id="ext-use-time" style="margin-right:8px;"> Scan new since last run
</label>
<div id="ext-last-run" style="font-size:10px; color:#666; margin-left:20px; margin-bottom: 5px;"></div>
<label style="cursor:pointer; display:flex; align-items:center; font-size:12px; color: #4db8ff;">
<input type="checkbox" id="ext-auto-images" style="margin-right:8px;"> Auto-open Last Image (Detail Pg)
</label>
</div>
<div style="display:flex; gap:10px;">
<button id="ext-start-btn" style="flex:1; background:#28a745; color:white; border:none; padding:8px; border-radius:4px; cursor:pointer; font-weight:bold;">START</button>
<button id="ext-stop-btn" style="flex:1; background:#dc3545; color:white; border:none; padding:8px; border-radius:4px; cursor:pointer; font-weight:bold;" disabled>STOP</button>
</div>
`;
document.body.appendChild(overlay);
document.getElementById('ext-add-btn').onclick = () => addExcludeWord(document.getElementById('ext-exclude-input').value);
document.getElementById('ext-start-btn').onclick = startScan;
document.getElementById('ext-stop-btn').onclick = stopScan;
const timeCheck = document.getElementById('ext-use-time');
timeCheck.checked = settings.useLastRunTime;
timeCheck.onchange = (e) => {
settings.useLastRunTime = e.target.checked;
saveSettings();
updateUI();
};
const imgCheck = document.getElementById('ext-auto-images');
imgCheck.checked = settings.autoImages;
imgCheck.onchange = (e) => {
settings.autoImages = e.target.checked;
saveSettings();
};
document.getElementById('ext-exclude-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') addExcludeWord(e.target.value);
});
updateUI();
}
function updateUI() {
const listContainer = document.getElementById('ext-exclude-list');
if (!listContainer) return;
settings.excludeWords.sort();
listContainer.innerHTML = settings.excludeWords.length === 0
? '<div style="color:#666; font-style:italic; width:100%;">No exclusions</div>'
: settings.excludeWords.map((w, i) => `
<div style="display:flex; align-items:center; background:#333; padding:2px 6px; border-radius:4px; font-size:11px; border:1px solid #444;">
<span>${w}</span>
<span style="color:#ff4444; cursor:pointer; font-weight:bold; margin-left:5px;" onclick="window.removeExtWord(${i})">×</span>
</div>
`).join('');
const lastRunDiv = document.getElementById('ext-last-run');
if (settings.lastRunTime) {
const minAgo = Math.floor((Date.now() - settings.lastRunTime) / 60000);
if (minAgo > 1440) {
const days = Math.floor(minAgo / 1440);
lastRunDiv.innerText = `${days} days ago`;
} else if (minAgo > 60) {
const hrs = Math.floor(minAgo / 60);
lastRunDiv.innerText = `${hrs} hours ago`;
} else {
lastRunDiv.innerText = `${minAgo} mins ago`;
}
} else {
lastRunDiv.innerText = 'Never run';
}
}
// --- DETAIL PAGE IMAGE LOGIC ---
let hasOpenedImages = false;
function checkAndOpenImages() {
if (!settings.autoImages || hasOpenedImages) return;
const links = Array.from(document.querySelectorAll('a'));
const imageUrls = new Set();
links.forEach(link => {
const text = link.innerText.trim();
const href = link.href;
// Check against allowed domains
const textMatches = IMAGE_DOMAINS.some(domain => text.includes(domain));
const hrefMatches = href && IMAGE_DOMAINS.some(domain => href.includes(domain));
if (textMatches && text.startsWith('http')) {
imageUrls.add(text);
} else if (hrefMatches) {
imageUrls.add(href);
}
});
const urlArray = Array.from(imageUrls);
if (urlArray.length > 0) {
hasOpenedImages = true;
const lastUrl = urlArray[urlArray.length - 1];
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed; top: 20px; right: 20px; background: #28a745; color: white;
padding: 10px 20px; border-radius: 5px; z-index: 10000; font-weight: bold;
box-shadow: 0 4px 10px rgba(0,0,0,0.3); animation: fadein 0.5s; font-size: 14px;
`;
toast.innerText = `📷 Opening last sample image...`;
document.body.appendChild(toast);
setTimeout(() => { if (toast.isConnected) toast.remove(); }, 4000);
if (typeof GM_openInTab === 'function') {
GM_openInTab(lastUrl, { active: false, insert: true });
} else {
window.open(lastUrl, '_blank');
}
}
}
// --- CORE LOGIC ---
document.addEventListener('mouseup', (e) => {
const sel = window.getSelection().toString().trim();
const existing = document.getElementById('ext-sel-popup');
if (existing) existing.remove();
if (sel.length > 1 && sel.length < 50) {
selectedTextBuffer = sel;
const btn = document.createElement('button');
btn.id = 'ext-sel-popup';
btn.innerHTML = `+ Exclude "<b>${sel}</b>"`;
btn.style.cssText = `
position: absolute; top: ${e.pageY - 40}px; left: ${e.pageX}px;
background: #dc3545; color: white; border: none; padding: 5px 10px;
border-radius: 4px; cursor: pointer; z-index: 10000; font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.3); pointer-events: auto;
`;
btn.onmousedown = (evt) => {
evt.preventDefault(); evt.stopPropagation();
addExcludeWord(selectedTextBuffer);
btn.remove();
window.getSelection().removeAllRanges();
};
document.body.appendChild(btn);
setTimeout(() => { if(btn.isConnected) btn.remove(); }, 3000);
}
});
function addExcludeWord(word) {
if (!word) return;
word = word.toLowerCase().trim();
if (!settings.excludeWords.includes(word)) {
settings.excludeWords.push(word);
saveSettings();
updateUI();
const input = document.getElementById('ext-exclude-input');
if(input) input.value = '';
}
}
window.removeExtWord = function(index) {
settings.excludeWords.splice(index, 1);
saveSettings();
updateUI();
};
// --- SCANNER LOGIC ---
function parseRow(row) {
const link = row.querySelector('td:first-child a');
const age = row.querySelector('td:nth-child(4)');
if (!link) return null;
const title = link.innerText.trim();
const res = extractResolution(title);
const norm = normalizeTitle(title);
return {
element: row,
title: title,
url: link.href,
ageText: age ? age.innerText.trim() : "",
resolution: res,
normalizedTitle: norm,
isOpened: false,
duplicateExcluded: false // State for resolution check
};
}
// Filter duplicates BEFORE scanning
function filterDuplicates(queue) {
let groups = {};
queue.forEach(item => {
if (!groups[item.normalizedTitle]) groups[item.normalizedTitle] = [];
groups[item.normalizedTitle].push(item);
});
for (let key in groups) {
let group = groups[key];
if (group.length > 1) {
// Sort descending by resolution (highest first)
group.sort((a, b) => b.resolution - a.resolution);
// Keep index 0 (Highest), mark others as excluded
for (let i = 1; i < group.length; i++) {
group[i].duplicateExcluded = true;
markRow(group[i].element, 'duplicate', `Duplicate (Lower Res: ${group[i].resolution}p)`);
}
}
}
}
function isExcluded(item) {
if (item.duplicateExcluded) return { excluded: true, reason: "Duplicate (Lower Res)" };
const titleLower = item.title.toLowerCase();
for (let word of settings.excludeWords) {
if (titleLower.includes(word)) return { excluded: true, reason: `Word: ${word}` };
}
if (settings.useLastRunTime && settings.lastRunTime) {
const text = item.ageText.toLowerCase();
const now = Date.now();
let itemTime = now;
const numMatch = text.match(/(\d+)/);
const num = numMatch ? parseInt(numMatch[1]) : 0;
if (text.includes('min')) itemTime = now - (num * 60 * 1000);
else if (text.includes('hour')) itemTime = now - (num * 60 * 60 * 1000);
else if (text.includes('day')) itemTime = now - (num * 24 * 60 * 60 * 1000);
else if (text.includes('month') || text.includes('year')) itemTime = now - (100 * 24 * 60 * 60 * 1000);
if (itemTime < (settings.lastRunTime - 300000)) {
return { excluded: true, reason: "Too old" };
}
}
return { excluded: false };
}
function markRow(row, type, msg) {
const existing = row.querySelector('.ext-marker');
if(existing) existing.remove();
const tag = document.createElement('span');
tag.className = 'ext-marker';
tag.style.cssText = `font-size: 10px; font-weight: bold; padding: 2px 4px; border-radius: 3px; margin-left: 8px;`;
if (type === 'good') {
row.style.backgroundColor = 'rgba(40, 167, 69, 0.15)';
tag.style.backgroundColor = '#28a745';
tag.style.color = '#fff';
tag.innerText = "✓ OPENING";
} else if (type === 'bad') {
row.style.backgroundColor = 'rgba(220, 53, 69, 0.1)';
tag.style.backgroundColor = '#dc3545';
tag.style.color = '#fff';
tag.innerText = `✕ ${msg}`;
row.style.opacity = '0.6';
} else if (type === 'duplicate') {
row.style.backgroundColor = 'rgba(255, 193, 7, 0.1)'; // Yellow tint
tag.style.backgroundColor = '#ffc107';
tag.style.color = '#000';
tag.innerText = `⚠ ${msg}`;
row.style.opacity = '0.7';
}
const titleCell = row.querySelector('td:first-child');
if (titleCell) titleCell.appendChild(tag);
}
// --- QUEUE & EXECUTION ---
function updateTimestamp() {
settings.lastRunTime = Date.now();
saveSettings();
updateUI();
}
function startScan() {
if (state.isScanning) return;
const rows = document.querySelectorAll('table tbody tr');
state.scanQueue = [];
rows.forEach(r => {
const item = parseRow(r);
if(item) state.scanQueue.push(item);
});
if (state.scanQueue.length === 0) return;
filterDuplicates(state.scanQueue);
state.isScanning = true;
state.queueIndex = 0;
state.stats = { opened: 0, excluded: 0, total: 0 };
document.getElementById('ext-start-btn').disabled = true;
document.getElementById('ext-stop-btn').disabled = false;
document.getElementById('scan-status').innerText = "Scanning...";
processQueue();
}
function stopScan() {
state.isScanning = false;
updateTimestamp();
document.getElementById('ext-start-btn').disabled = false;
document.getElementById('ext-stop-btn').disabled = true;
document.getElementById('scan-status').innerText = "Stopped";
}
function processQueue() {
if (!state.isScanning) return;
if (state.queueIndex >= state.scanQueue.length) {
finishScan();
return;
}
const item = state.scanQueue[state.queueIndex];
const check = isExcluded(item);
if (check.excluded) {
if (!item.duplicateExcluded) {
markRow(item.element, 'bad', check.reason);
}
state.stats.excluded++;
state.queueIndex++;
setTimeout(processQueue, 5);
} else {
markRow(item.element, 'good');
if (typeof GM_openInTab === 'function') {
GM_openInTab(item.url, { active: false, insert: true });
} else {
window.open(item.url, '_blank');
}
state.stats.opened++;
state.queueIndex++;
document.getElementById('scan-status').innerText = `Opening ${state.stats.opened}...`;
setTimeout(processQueue, CONFIG.scanDelay);
}
}
function finishScan() {
state.isScanning = false;
updateTimestamp();
document.getElementById('ext-start-btn').disabled = false;
document.getElementById('ext-stop-btn').disabled = true;
document.getElementById('scan-status').innerText = `Done. Opened: ${state.stats.opened}`;
}
// --- INITIALIZATION ---
createOverlay();
checkAndOpenImages();
setTimeout(checkAndOpenImages, 1000);
setTimeout(checkAndOpenImages, 3000);
})();