CircleFTP Dub And CamRip Labeler

Adds DUB and CAM labels to thumbnails on CircleFTP

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         CircleFTP Dub And CamRip Labeler
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Adds DUB and CAM labels to thumbnails on CircleFTP
// @author       LaxyDevUserX
// @match        http://new.circleftp.net/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Keywords that indicate dubbed content
    const dubKeywords = [
        'Dual Audio', 'Dubbed', 'Dub', 'Multi Audio',
        'Hin+Eng', 'Eng+Jap', 'Hin+Tam', 'Hin+Mal', 'Hin+Tel',
        'Hin+Kor', 'Eng+Hin', 'Jap+Eng', 'Kor+Hin', 'Chi+Eng',
        'Eng+Chi', 'Tam+Eng', 'Mal+Eng', 'Tel+Eng', 'Tur+Hin'
    ];

    // Keywords that indicate cam/low quality sources
    const camRipKeywords = [
        'PRE HDRip', 'Cam Rip', 'CAM', 'TS', 'TC', 'HDTS', 'HDCAM',
        'HDTC', 'CAMRip', 'DVDSCR', 'SCR', 'TELESYNC', 'TELECINE',
        'PDVD', 'Workprint', 'WP', 'PDTV', 'DSR', 'STV'
    ];

    // Cache for processed cards to avoid reprocessing
    const processedCards = new Set();

    // Add CSS for the labels
    const style = document.createElement('style');
    style.textContent = `
        .dub-label {
            position: absolute;
            top: 5px;
            right: 5px;
            background-color: #f4181c;
            color: white;
            padding: 4px 10px;
            border-radius: 4px;
            font-weight: bold;
            font-size: 12px;
            z-index: 9999;
            box-shadow: 0 2px 6px rgba(0,0,0,0.8);
            text-transform: uppercase;
            letter-spacing: 0.5px;
            pointer-events: none;
            animation: fadeIn 0.3s ease-in;
            border: 1px solid rgba(255,255,255,0.3);
        }

        .dub-blue {
            background-color: #5C33F6;
        }

        .cam-rip-label {
            position: absolute;
            top: 5px;
            left: 5px;
            background-color: #D72638; /* Red for Cam Rip */
            color: white;
            padding: 4px 10px;
            border-radius: 4px;
            font-weight: bold;
            font-size: 12px;
            z-index: 9999;
            box-shadow: 0 2px 6px rgba(0,0,0,0.8);
            text-transform: uppercase;
            letter-spacing: 0.5px;
            pointer-events: none;
            animation: fadeIn 0.3s ease-in;
            border: 1px solid rgba(255,255,255,0.3);
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: scale(0.8); }
            to { opacity: 1; transform: scale(1); }
        }

        /* Ensure image containers have relative positioning */
        .SinglePost_singlePost_card__MLfCk > a > div {
            position: relative !important;
        }
    `;
    document.head.appendChild(style);

    // Function to check if text contains dub keywords
    function containsDubKeywords(text) {
        if (!text) return { hasDub: false, isBlue: false };

        const hasDub = dubKeywords.some(keyword => text.includes(keyword));

        // Check for specific combination: both "Dual Audio" and "Eng+Jap"
        const hasDualAudio = text.includes('Dual Audio');
        const hasEngJap = text.includes('Eng+Jap');
        const isBlue = hasDualAudio && hasEngJap;

        return { hasDub, isBlue };
    }

    // Function to check if text contains cam rip keywords
    function containsCamRipKeywords(text) {
        if (!text) return false;
        return camRipKeywords.some(keyword => text.includes(keyword));
    }

    // Function to add labels to a card
    function addLabels(card) {
        // Get a unique identifier for the card (using the href)
        const linkElement = card.querySelector('a');
        const cardId = linkElement ? linkElement.href : null;

        // Skip if already processed
        if (!cardId || processedCards.has(cardId)) return;

        // Check multiple places for keywords:
        // 1. The title attribute of the card div
        const cardTitle = card.getAttribute('title') || '';

        // 2. The h3 text content
        const h3Element = card.querySelector('h3');
        const h3Text = h3Element ? h3Element.textContent : '';

        // 3. The p tag text content
        const pElement = card.querySelector('p');
        const pText = pElement ? pElement.textContent : '';

        // Combine all text for checking
        const allText = `${cardTitle} ${h3Text} ${pText}`;

        // Check for dub keywords
        const { hasDub, isBlue } = containsDubKeywords(allText);

        // Check for cam rip keywords
        const hasCamRip = containsCamRipKeywords(allText);

        if (hasDub || hasCamRip) {
            // Find the image container
            const imageContainer = card.querySelector('.overflow-hidden.d-flex.justify-content-center.align-items-end.rounded');
            if (!imageContainer) {
                // Try alternative selector
                const img = card.querySelector('img.SinglePost_singlePost_image__roLcd');
                if (!img) return;

                const imageContainerAlt = img.parentElement;
                if (!imageContainerAlt) return;

                createAndAddLabels(imageContainerAlt, hasDub, isBlue, hasCamRip);
            } else {
                createAndAddLabels(imageContainer, hasDub, isBlue, hasCamRip);
            }

            // Mark as processed
            processedCards.add(cardId);
        }
    }

    function createAndAddLabels(container, hasDub, isBlue, hasCamRip) {
        // Add CAM RIP label if needed (left side)
        if (hasCamRip) {
            const camRipLabel = document.createElement('div');
            camRipLabel.className = 'cam-rip-label';
            camRipLabel.textContent = 'CAM';
            container.appendChild(camRipLabel);
        }

        // Add DUB label if needed (right side)
        if (hasDub) {
            const dubLabel = document.createElement('div');
            dubLabel.className = `dub-label ${isBlue ? 'dub-blue' : ''}`;
            dubLabel.textContent = 'DUB';
            container.appendChild(dubLabel);
        }
    }

    // Set up IntersectionObserver for lazy loading
    const observerOptions = {
        root: null, // viewport
        rootMargin: '200px', // start loading 200px before element comes into view
        threshold: 0.1 // trigger when 10% of element is visible
    };

    const intersectionObserver = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const card = entry.target;
                addLabels(card);
                // Stop observing this card once processed
                intersectionObserver.unobserve(card);
            }
        });
    }, observerOptions);

    // Function to observe all cards
    function observeAllCards() {
        const cards = document.querySelectorAll('.SinglePost_singlePost_card__MLfCk');
        cards.forEach(card => {
            // Get a unique identifier for the card
            const linkElement = card.querySelector('a');
            const cardId = linkElement ? linkElement.href : null;

            // Only observe if not already processed
            if (cardId && !processedCards.has(cardId)) {
                intersectionObserver.observe(card);
            }
        });
    }

    // Optimized observer for dynamic content
    let mutationTimeout;
    function setupMutationObserver() {
        const observer = new MutationObserver(mutations => {
            // Clear any pending timeout
            clearTimeout(mutationTimeout);

            // Set a new timeout to batch mutations
            mutationTimeout = setTimeout(() => {
                observeAllCards();
            }, 300); // 300ms delay to batch mutations
        });

        // Observe the entire document for added nodes
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Initialize the script
    function init() {
        // Process initial content after a short delay
        setTimeout(() => {
            observeAllCards();
        }, 500); // Reduced delay for faster initial loading

        // Setup mutation observer for dynamic content
        setTimeout(setupMutationObserver, 1000);
    }

    // Run the script when the page is loaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();