SimpCity Watched Thread Media Scanner

Scans watched threads, looks for the "New" badge, jumps to the last page, and opens posts with media (links, videos, images) in a new tab.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         SimpCity Watched Thread Media Scanner
// @namespace    GeminiScripts
// @version      44.0
// @description  Scans watched threads, looks for the "New" badge, jumps to the last page, and opens posts with media (links, videos, images) in a new tab.
// @author       Gemini
// @match        *://simpcity.su/*
// @match        *://simpcity.cr/*
// @match        *://simpcity.li/*
// @icon         
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const SCAN_DELAY_MS = 2500;
    const OPEN_DELAY_MS = 2000;
    const BUTTON_ID = 'gemini-float-scan-btn';

    let isScanning = false;

    // --- HOTKEY (Alt + S) ---
    document.addEventListener('keydown', (e) => {
        if (e.altKey && (e.code === 'KeyS' || e.key === 's')) {
            e.preventDefault();
            if (isScanning) stopScan();
            else startScan();
        }
    });

    // --- INJECTION: FLOATING BUTTON ---
    setInterval(() => {
        if (!window.location.href.includes('/watched/threads')) return;
        if (document.getElementById(BUTTON_ID)) return;
        createFloatingButton();
    }, 1000);

    function createFloatingButton() {
        const btn = document.createElement('div');
        btn.id = BUTTON_ID;
        btn.innerText = 'SCAN MEDIA';
        
        Object.assign(btn.style, {
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            zIndex: '9999',
            padding: '12px 20px',
            backgroundColor: '#e67e22',
            color: 'white',
            fontWeight: 'bold',
            borderRadius: '50px',
            boxShadow: '0 4px 6px rgba(0,0,0,0.3)',
            cursor: 'pointer',
            fontSize: '14px',
            fontFamily: 'Arial, sans-serif',
            border: '2px solid #d35400',
            transition: 'all 0.3s ease'
        });

        btn.onmouseover = () => { btn.style.transform = 'scale(1.05)'; };
        btn.onmouseout = () => { btn.style.transform = 'scale(1.0)'; };

        btn.onclick = (e) => {
            e.preventDefault();
            if (isScanning) stopScan();
            else startScan();
        };

        document.body.appendChild(btn);
    }

    // --- STATUS UPDATE ---
    function updateStatus(text, color, bgColor) {
        const btn = document.getElementById(BUTTON_ID);
        if (btn) {
            btn.innerText = text;
            if (color) btn.style.color = color;
            if (bgColor) {
                btn.style.backgroundColor = bgColor;
                btn.style.borderColor = bgColor;
            }
        }
    }

    function stopScan() {
        isScanning = false;
        updateStatus('STOPPING...', 'white', '#7f8c8d');
        setTimeout(() => {
            updateStatus('SCAN MEDIA', 'white', '#e67e22');
        }, 1000);
    }

    // --- MAIN LOGIC ---
    async function startScan() {
        isScanning = true;
        updateStatus('STOP SCAN', 'white', '#c0392b'); 
        
        // Find all threads that have an "unread" indicator
        const rows = document.querySelectorAll('.structItem--thread');
        const unreadRows = Array.from(rows).filter(row => row.querySelector('.structItem-title a[href*="/unread"]'));

        if (unreadRows.length === 0) {
            updateStatus('NO NEW', 'white', '#e67e22');
            isScanning = false;
            setTimeout(() => { updateStatus('SCAN MEDIA', 'white', '#e67e22'); }, 2000);
            return;
        }

        for (let i = 0; i < unreadRows.length; i++) {
            if (!isScanning) return; 

            const row = unreadRows[i];
            
            // --- SMART URL SELECTION (Last Page Logic) ---
            // 1. Default: The unread link
            let targetLink = row.querySelector('.structItem-title a[href*="/unread"]').href;
            
            // 2. Optimization: Check for Page Numbers
            // We want the LAST page (highest number)
            const pageJumpContainer = row.querySelector('.structItem-pageJump');
            if (pageJumpContainer) {
                const lastPageLink = pageJumpContainer.querySelector('a:last-child');
                if (lastPageLink) {
                    targetLink = lastPageLink.href;
                }
            }

            if (targetLink) {
                updateStatus(`${i + 1} / ${unreadRows.length}`, 'white', '#e67e22');

                try {
                    const result = await checkUrlForMedia(targetLink);
                    if (!isScanning) return;

                    if (result.hasMedia) {
                        row.style.backgroundColor = 'rgba(40, 167, 69, 0.2)'; 
                        row.style.borderLeft = '5px solid #28a745';
                        row.style.opacity = '1.0';

                        updateStatus('OPENING...', 'white', '#2ecc71');
                        GM_openInTab(result.specificUrl, { active: false, insert: true });
                        await sleep(OPEN_DELAY_MS);
                    } else {
                        row.style.opacity = '0.3'; 
                        row.style.filter = 'grayscale(100%)';
                        await sleep(SCAN_DELAY_MS);
                    }
                } catch (err) {
                    await sleep(1000); 
                }
            }
        }

        if (isScanning) {
            updateStatus('DONE', 'white', '#2ecc71');
            setTimeout(() => {
                isScanning = false;
                updateStatus('SCAN MEDIA', 'white', '#e67e22');
            }, 3000);
        }
    }

    // --- HELPER: CHECK URL (COOKIES + STRICT BADGE) ---
    function checkUrlForMedia(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                withCredentials: true, // Sends Cookies to see badges
                onload: function(response) {
                    if (!isScanning) { resolve({ hasMedia: false }); return; }

                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, "text/html");
                    const finalUrlBase = (response.finalUrl || url).split('#')[0]; 
                    
                    const posts = doc.querySelectorAll('.message'); 
                    let result = { hasMedia: false, specificUrl: null };

                    // SIGNATURES for Media Detection
                    const MEDIA_SIGNATURES = [
                        'turbo.cr', 'gofile.io', 'pixeldrain', 'mega.nz', 'saint.to', 'bunkr', 
                        'transfer.it', 'catbox.moe', 'simpcity.su', 'simpcity.cr', 
                        'redgifs', 'imgbox', 'filedit', 'cyberdrop', 'qiwi', 'krakenfiles',
                        '.mp4', '.mkv', '.webm', '.m3u8', '.mov', 
                        'jwplayer', 'video-js', 'plyr', 'class="video', 'tag="video"',
                        'img src=', 'attachment', 'bbcodespoiler'
                    ];

                    for (let post of posts) {
                        // --- STRICT NEW BADGE CHECK ---
                        const isNew = post.innerHTML.includes('message-newIndicator');
                        
                        // SKIP OLD POSTS
                        if (!isNew) continue; 

                        // --- CONTENT SCAN ---
                        const body = post.querySelector('.message-body');
                        if (!body) continue;

                        const clone = body.cloneNode(true);
                        clone.querySelectorAll('.bbCodeBlock--quote').forEach(q => q.remove());
                        clone.querySelectorAll('.smilie').forEach(s => s.remove());

                        let isMedia = false;
                        const rawHtml = clone.innerHTML.toLowerCase();

                        // A. Check for SIGNATURES (Raw HTML)
                        if (MEDIA_SIGNATURES.some(sig => rawHtml.includes(sig))) isMedia = true;

                        // B. Standard Checks (Tags)
                        if (!isMedia) {
                            if (clone.querySelector('.bbCodeSpoiler-button, .bbCodeInlineSpoiler')) isMedia = true;
                            if (clone.querySelector('video, iframe, object, embed')) isMedia = true;
                            if (clone.querySelector('img.bbImage')) isMedia = true;
                            
                            // Regex for ANY external link
                            if (/(https?:\/\/(?!(\w+\.)?simpcity))/i.test(rawHtml)) isMedia = true;
                        }

                        if (isMedia) {
                            result.hasMedia = true;
                            let anchorId = post.id; 
                            if (!anchorId) {
                                const permalink = post.querySelector('a[data-content-selector^="#post-"]');
                                if (permalink) anchorId = permalink.getAttribute('data-content-selector').replace('#', '');
                            }
                            result.specificUrl = anchorId ? `${finalUrlBase}#${anchorId}` : finalUrlBase;
                            break; 
                        }
                    }
                    resolve(result);
                },
                onerror: function() { resolve({ hasMedia: false, specificUrl: null }); }
            });
        });
    }

    function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

})();