EXT Torrent Scanner (v3.3)

Scan torrents, exclude keywords, auto-open. Auto-opens ONLY the last sample image. Auto-rejects lower resolution duplicates. 4s Delay.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         EXT 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);

})();