PT站电影评分过滤器

可自定义过滤条件的电影过滤器,支持多个PT站点

// ==UserScript==
// @name         PT站电影评分过滤器
// @namespace    http://tampermonkey.net/
// @version      1.6.2
// @description  可自定义过滤条件的电影过滤器,支持多个PT站点
// @author       Dost
// @match        https://ubits.club/torrents.php*
// @match        https://cyanbug.net/torrents.php*
// @match        https://hdfans.org/torrents.php*
// @match        https://carpt.net/torrents.php*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 默认配置
    const DEFAULT_CONFIG = {
        minIMDbRating: 6.1,
        minDoubanRating: 6.5,
        removeNA: false,
        removeOnlyAllNA: true,
        requireBothRatings: false,
        showDebugInfo: true,
        enabled: true,
        showNotification: true,
        notificationDuration: 8
    };

    // 公共选择器配置
    const COMMON_SELECTORS = {
        imdbSelector: 'div:nth-child(1) > span',
        doubanSelector: 'div:nth-child(2) > span',
        ratingContainerSelector: 'div[style*="flex-direction: column"]'
    };

/* 如何适配新站点?
1. 在@match添加新站点URL(如// @match https://new-site.com/torrents.php*)
2. 在SITE_ADAPTERS中添加适配器配置:
   'new-site.com': {
     name: '站点显示名称',
     rowSelector: 'tr:has(> td > 评分容器父元素)',
     ratingContainerSelector: '包含双评分的div',
     imdbSelector: 'IMDb评分元素选择器',
     doubanSelector: '豆瓣评分元素选择器'
   }
3. 选择器调试技巧:
   - 用开发者工具检查评分区域HTML结构
   - 优先尝试复用现有选择器
   - 开启showDebugInfo查看过滤日志
*/
    // 站点适配器配置
    const SITE_ADAPTERS = {
        'ubits.club': {
            name: 'UBits',
            rowSelector: 'tr:has(> td > table.torrentname > tbody > tr > td.embedded > div[style*="flex-direction: column"])',
            ...COMMON_SELECTORS
        },
        'cyanbug.net': {
            name: 'CyanBug',
            rowSelector: 'tr:has(> td > table.torrentname > tbody > tr > td.embedded > div[style*="flex-direction: column"])',
            ...COMMON_SELECTORS
        },
        'hdfans.org': {
            name: 'HDFans',
            rowSelector: 'tr:has(> td > table.torrentname > tbody > tr > td.embedded > div[style*="flex-direction: column"])',
            ...COMMON_SELECTORS
        },
        'carpt.net': {
            name: 'CARPT',
            rowSelector: 'tr:has(> td > table.torrentname > tbody > tr > td.embedded > div[style*="flex-direction: column"])',
            ...COMMON_SELECTORS
        }
    };

    // 主控制器
    class FilterController {
        config = { ...DEFAULT_CONFIG };
        adapter = SITE_ADAPTERS[location.hostname];
        stats = {
            totalChecked: 0,
            totalRemoved: 0,
            removedByLowIMDb: 0,
            removedByLowDouban: 0,
            removedByNA: 0,
            removedByMissingRating: 0
        };

        constructor() {
            this.loadConfig();
            this.initStyles();
        }

        loadConfig() {
            const saved = GM_getValue('MultiSiteFilterConfig', '{}');
            this.config = {
                ...DEFAULT_CONFIG,
                ...(typeof saved === 'string' ? JSON.parse(saved) : saved)
            };
        }

        initStyles() {
            const style = document.createElement('style');
            style.textContent = `
                .multisite-filter-btn {
                    position: fixed;
                    z-index: 9998;
                    padding: 8px 15px;
                    color: white;
                    border: none;
                    border-radius: 20px;
                    cursor: pointer;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.2);
                    font-family: Arial, sans-serif;
                }
                .multisite-filter-config-btn {
                    bottom: 70px;
                    right: 20px;
                    background: #6c757d;
                }
                .multisite-filter-toggle-btn {
                    bottom: 20px;
                    right: 20px;
                    background: #17a2b8;
                }
                #multisite-filter-notification {
                    position: fixed;
                    top: 10px;
                    right: 10px;
                    background-color: #f8f9fa;
                    color: #212529;
                    padding: 12px;
                    border-radius: 5px;
                    z-index: 9999;
                    box-shadow: 0 0 15px rgba(0,0,0,0.2);
                    max-width: 320px;
                    font-family: Arial, sans-serif;
                    font-size: 14px;
                    line-height: 1.5;
                    white-space: pre-line;
                    border-left: 4px solid #6c757d;
                    transform: translateX(120%);
                    transition: transform 0.3s ease-out;
                }
                #multisite-filter-config-dialog {
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: white;
                    padding: 20px;
                    z-index: 10000;
                    box-shadow: 0 0 20px rgba(0,0,0,0.3);
                    border-radius: 8px;
                    width: 350px;
                    max-width: 90%;
                    font-family: Arial, sans-serif;
                }
            `;
            document.head.appendChild(style);
        }

init() {
    // 即使禁用也应该添加控制按钮
    GM_registerMenuCommand(`配置${this.adapter.name}电影过滤器`, () => this.showConfigUI());
    this.addControlButtons();
    
    if (!this.config.enabled) {
        console.log(`${this.adapter.name}电影过滤器已禁用`);
        return;
    }

    this.applyFilters();
}
        applyFilters() {
            document.querySelectorAll(this.adapter.rowSelector).forEach(tr => {
                this.stats.totalChecked++;
                const ratingContainer = tr.querySelector(this.adapter.ratingContainerSelector);
                if (!ratingContainer) return;

                const imdbRating = this.parseRating(
                    ratingContainer.querySelector(this.adapter.imdbSelector)?.textContent
                );
                const doubanRating = this.parseRating(
                    ratingContainer.querySelector(this.adapter.doubanSelector)?.textContent
                );

                if (this.shouldRemoveItem(imdbRating, doubanRating, tr)) {
                    tr.style.display = 'none';
                    tr.dataset.filtered = 'true';
                    this.stats.totalRemoved++;
                }
            });

            this.showResults();
        }

        parseRating(ratingText) {
            if (!ratingText) return { valid: false, isNA: true };

            const text = ratingText.trim();
            if (text === 'N/A' || text === '' || text === '-') {
                return { valid: false, isNA: true };
            }

            const value = parseFloat(text);
            return isNaN(value)
                ? { valid: false, isNA: true }
                : { valid: true, isNA: false, value };
        }

        shouldRemoveItem(imdbRating, doubanRating, tr) {
            let shouldRemove = false;
            const removeReasons = [];

            // 检查双评分要求
            if (this.config.requireBothRatings && (!imdbRating.valid || !doubanRating?.valid)) {
                shouldRemove = true;
                removeReasons.push('缺少有效评分');
                this.stats.removedByMissingRating++;
            }

            // 检查N/A
            if (this.config.removeNA) {
                const imdbNA = imdbRating.isNA;
                const doubanNA = doubanRating ? doubanRating.isNA : true;

                if (this.config.removeOnlyAllNA ? imdbNA && doubanNA : imdbNA || doubanNA) {
                    shouldRemove = true;
                    removeReasons.push(this.config.removeOnlyAllNA ? '双评分均为N/A' : '存在N/A评分');
                    this.stats.removedByNA++;
                }
            }

            // 检查评分
            if (imdbRating.valid && this.config.minIMDbRating > 0 && imdbRating.value < this.config.minIMDbRating) {
                shouldRemove = true;
                removeReasons.push(`IMDb ${imdbRating.value} < ${this.config.minIMDbRating}`);
                this.stats.removedByLowIMDb++;
            }

            if (doubanRating?.valid && this.config.minDoubanRating > 0 && doubanRating.value < this.config.minDoubanRating) {
                shouldRemove = true;
                removeReasons.push(`豆瓣 ${doubanRating.value} < ${this.config.minDoubanRating}`);
                this.stats.removedByLowDouban++;
            }

            if (shouldRemove && this.config.showDebugInfo) {
                console.log(`删除项目: ${removeReasons.join('; ')}`, tr);
            }

            return shouldRemove;
        }

        addControlButtons() {
            // 移除旧按钮
            document.querySelectorAll('#multisite-filter-config-btn, #multisite-filter-toggle-btn')
                   .forEach(btn => btn.remove());

            // 配置按钮
            const configBtn = document.createElement('button');
            configBtn.id = 'multisite-filter-config-btn';
            configBtn.className = 'multisite-filter-btn multisite-filter-config-btn';
            configBtn.textContent = `⚙️ ${this.adapter.name}过滤器配置`;
            configBtn.addEventListener('click', () => this.showConfigUI());

            // 切换按钮
            const toggleBtn = document.createElement('button');
            toggleBtn.id = 'multisite-filter-toggle-btn';
            toggleBtn.className = 'multisite-filter-btn multisite-filter-toggle-btn';
            toggleBtn.textContent = '👁️ 显示被过滤';

            let showFiltered = false;
            toggleBtn.addEventListener('click', () => {
                showFiltered = !showFiltered;
                toggleBtn.textContent = showFiltered ? '👁️ 隐藏被过滤' : '👁️ 显示被过滤';
                this.toggleFilteredItems(showFiltered);
            });

            document.body.append(configBtn, toggleBtn);
        }

        toggleFilteredItems(show) {
            document.querySelectorAll('tr[data-filtered="true"]')
                   .forEach(row => row.style.display = show ? '' : 'none');
        }

        showResults() {
            const resultLines = [
                `${this.adapter.name}电影过滤结果 (共检查 ${this.stats.totalChecked} 个项目)`,
                `-------------------------------------`,
                `隐藏总数: ${this.stats.totalRemoved}`,
                ...(this.stats.removedByLowIMDb > 0 ? [`- IMDb评分过低: ${this.stats.removedByLowIMDb}`] : []),
                ...(this.stats.removedByLowDouban > 0 ? [`- 豆瓣评分过低: ${this.stats.removedByLowDouban}`] : []),
                ...(this.stats.removedByNA > 0 ? [`- N/A评分: ${this.stats.removedByNA}`] : []),
                ...(this.stats.removedByMissingRating > 0 ? [`- 缺少有效评分: ${this.stats.removedByMissingRating}`] : []),
                `-------------------------------------`,
                `当前过滤条件:`,
                `- 最低IMDb评分: ${this.config.minIMDbRating > 0 ? this.config.minIMDbRating : '不限制'}`,
                `- 最低豆瓣评分: ${this.config.minDoubanRating > 0 ? this.config.minDoubanRating : '不限制'}`,
                `- 删除N/A: ${this.config.removeNA ? (this.config.removeOnlyAllNA ? '仅双N/A' : '任意N/A') : '否'}`,
                `- 要求双评分: ${this.config.requireBothRatings ? '是' : '否'}`,
                `- 过滤器状态: ${this.config.enabled ? '启用' : '禁用'}`
            ];

            console.log(resultLines.join('\n'));
            if (this.config.showNotification) {
                this.showNotification(resultLines.join('\n'), this.config.notificationDuration * 1000);
            }
        }

        showNotification(message, duration = 8000) {
            const existing = document.getElementById('multisite-filter-notification');
            if (existing) existing.remove();

            const notification = document.createElement('div');
            notification.id = 'multisite-filter-notification';
            notification.textContent = message;
            document.body.appendChild(notification);

            setTimeout(() => notification.style.transform = 'translateX(0)', 100);

            const hideTimer = setTimeout(() => {
                notification.style.transition = 'opacity 1s';
                notification.style.opacity = '0';
                setTimeout(() => notification.remove(), 1000);
            }, duration);

            notification.addEventListener('mouseenter', () => clearTimeout(hideTimer));
            notification.addEventListener('mouseleave', () => {
                notification.style.opacity = '1';
                setTimeout(() => {
                    notification.style.transition = 'opacity 1s';
                    notification.style.opacity = '0';
                    setTimeout(() => notification.remove(), 1000);
                }, duration);
            });
        }

        showConfigUI() {
            const existing = document.getElementById('multisite-filter-config-dialog');
            if (existing) existing.remove();

            const dialog = document.createElement('div');
            dialog.id = 'multisite-filter-config-dialog';
            dialog.innerHTML = `
                <h3 style="margin-top:0;color:#495057">${this.adapter.name}电影过滤器配置</h3>
                <div style="margin-bottom:15px">
                    <label style="display:flex;align-items:center">
                        <input type="checkbox" id="multisite-filter-enabled" ${this.config.enabled ? 'checked' : ''} style="margin-right:8px">
                        启用过滤器
                    </label>
                </div>

                <div style="margin:20px 0;border-top:1px solid #eee;padding-top:15px">
                    <h4 style="margin:0 0 10px 0;color:#495057">过滤条件</h4>
                    <div style="margin-bottom:15px">
                        <label style="display:block;margin-bottom:5px;color:#495057">最低IMDb评分:</label>
                        <input type="number" id="multisite-filter-imdb" step="0.1" min="0" max="10" value="${this.config.minIMDbRating}" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:4px">
                    </div>
                    <div style="margin-bottom:15px">
                        <label style="display:block;margin-bottom:5px;color:#495057">最低豆瓣评分:</label>
                        <input type="number" id="multisite-filter-douban" step="0.1" min="0" max="10" value="${this.config.minDoubanRating}" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:4px">
                    </div>
                    <div style="margin-bottom:15px">
                        <label style="display:flex;align-items:center">
                            <input type="checkbox" id="multisite-filter-remove-na" ${this.config.removeNA ? 'checked' : ''} style="margin-right:8px">
                            只要包含N/A的项目就隐藏
                        </label>
                    </div>
                    <div style="margin-bottom:15px">
                        <label style="display:flex;align-items:center">
                            <input type="checkbox" id="multisite-filter-remove-only-all-na" ${this.config.removeOnlyAllNA ? 'checked' : ''} style="margin-right:8px" ${this.config.removeNA ? '' : 'disabled'}>
                            仅当双评分均为N/A时隐藏
                        </label>
                    </div>
                    <div style="margin-bottom:15px">
                        <label style="display:flex;align-items:center">
                            <input type="checkbox" id="multisite-filter-require-both" ${this.config.requireBothRatings ? 'checked' : ''} style="margin-right:8px">
                            必须同时包含IMDb和豆瓣评分
                        </label>
                    </div>
                </div>

                <div style="margin:20px 0;border-top:1px solid #eee;padding-top:15px">
                    <h4 style="margin:0 0 10px 0;color:#495057">通知设置</h4>
                    <div style="margin-bottom:15px">
                        <label style="display:flex;align-items:center">
                            <input type="checkbox" id="multisite-filter-show-notification" ${this.config.showNotification ? 'checked' : ''} style="margin-right:8px">
                            显示统计窗口
                        </label>
                    </div>
                    <div style="margin-bottom:15px">
                        <label style="display:block;margin-bottom:5px;color:#495057">统计窗口显示时间(秒):</label>
                        <input type="number" id="multisite-filter-notification-duration" min="1" max="60" value="${this.config.notificationDuration}" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:4px">
                    </div>
                </div>

                <div style="display:flex;justify-content:space-between;margin-top:20px">
                    <button id="multisite-filter-save" style="padding:8px 15px;background:#28a745;color:white;border:none;border-radius:4px;cursor:pointer">保存</button>
                    <button id="multisite-filter-cancel" style="padding:8px 15px;background:#6c757d;color:white;border:none;border-radius:4px;cursor:pointer">取消</button>
                </div>
            `;

            document.body.appendChild(dialog);

            document.getElementById('multisite-filter-remove-na').addEventListener('change', function() {
                document.getElementById('multisite-filter-remove-only-all-na').disabled = !this.checked;
            });

            document.getElementById('multisite-filter-save').addEventListener('click', () => {
    this.config = {
        enabled: document.getElementById('multisite-filter-enabled').checked,
        minIMDbRating: parseFloat(document.getElementById('multisite-filter-imdb').value) || 0,
        minDoubanRating: parseFloat(document.getElementById('multisite-filter-douban').value) || 0,
        removeNA: document.getElementById('multisite-filter-remove-na').checked,
        removeOnlyAllNA: document.getElementById('multisite-filter-remove-only-all-na').checked,
        requireBothRatings: document.getElementById('multisite-filter-require-both').checked,
        showNotification: document.getElementById('multisite-filter-show-notification').checked,
        notificationDuration: parseInt(document.getElementById('multisite-filter-notification-duration').value) || 8
    };

    GM_setValue('MultiSiteFilterConfig', JSON.stringify(this.config));
    dialog.remove();
    this.resetFilters();
    
    // 更新按钮状态
    this.addControlButtons();

    if (this.config.enabled) {
        this.applyFilters();
    } else if (this.config.showNotification) {
        this.showNotification(`${this.adapter.name}电影过滤器已禁用`);
    }
});

            document.getElementById('multisite-filter-cancel').addEventListener('click', () => dialog.remove());
        }

        resetFilters() {
            document.querySelectorAll('tr[data-filtered="true"]').forEach(row => {
                row.style.display = '';
                row.removeAttribute('data-filtered');
            });
        }
    }

    // 初始化
    const controller = new FilterController();
    window.addEventListener('load', () => controller.init());
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。