Bilibili-BlackList

屏蔽指定 UP 主的视频推荐,支持精确匹配和正则表达式匹配

// ==UserScript==
// Metadata block defining the script properties
// @name         Bilibili-BlackList
// @namespace     https://github.com/HeavenTTT/bilibili-blacklist
// @version      0.8.D
// @author       HeavenTTT
// @license no licnese
// @description  屏蔽指定 UP 主的视频推荐,支持精确匹配和正则表达式匹配
// @match        *://*.bilibili.com/*
// @grant        GM_setValue 
// @grant        GM_getValue 
// @grant        GM_addStyle   
// ==/UserScript==

(function () {
    'use strict';

    // 从存储中获取黑名单
    // Default exact match blacklist (case sensitive)
    let exactBlacklist = GM_getValue('exactBlacklist', ['绝区零','崩坏星穹铁道','崩坏3','原神','米哈游miHoYo']);
    // Default regex match blacklist
    let regexBlacklist = GM_getValue('regexBlacklist', ['王者荣耀.*','和平精英.*','PUBG.*','绝地求生.*','吃鸡.*']);

    // 保存黑名单到存储
    function saveBlacklists() {
        GM_setValue('exactBlacklist', exactBlacklist);
        GM_setValue('regexBlacklist', regexBlacklist);
    }

    // 创建黑名单管理面板
    function createBlacklistPanel() {
        // Create main panel container
        const panel = document.createElement('div');
        panel.id = 'bilibili-blacklist-panel';
        // Styling for the panel (centered modal)
        panel.style.position = 'fixed';
        panel.style.top = '50%';
        panel.style.left = '50%';
        panel.style.transform = 'translate(-50%, -50%)';
        panel.style.width = '500px';
        panel.style.maxHeight = '80vh';
        panel.style.backgroundColor = '#fff';
        panel.style.borderRadius = '8px';
        panel.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
        panel.style.zIndex = '99999';
        panel.style.overflow = 'hidden';
        panel.style.display = 'none';
        panel.style.flexDirection = 'column';

        // 选项卡
        const tabContainer = document.createElement('div');
        tabContainer.style.display = 'flex';
        tabContainer.style.borderBottom = '1px solid #f1f2f3';

        // Exact match tab
        const exactTab = document.createElement('div');
        exactTab.textContent = '精确匹配';
        exactTab.style.padding = '12px 16px';
        exactTab.style.cursor = 'pointer';
        exactTab.style.fontWeight = '500';
        exactTab.style.borderBottom = '2px solid #fb7299'; // Pink underline for active tab

        // Regex match tab
        const regexTab = document.createElement('div');
        regexTab.textContent = '正则匹配';
        regexTab.style.padding = '12px 16px';
        regexTab.style.cursor = 'pointer';

        tabContainer.appendChild(exactTab);
        tabContainer.appendChild(regexTab);

        // Panel header
        const header = document.createElement('div');
        header.style.padding = '16px';
        header.style.borderBottom = '1px solid #f1f2f3';
        header.style.display = 'flex';
        header.style.justifyContent = 'space-between';
        header.style.alignItems = 'center';

        const title = document.createElement('h3');
        title.textContent = '已屏蔽UP主';
        title.style.margin = '0';
        title.style.fontSize = '16px';
        title.style.fontWeight = '500';

        // Close button
        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        closeBtn.style.background = 'none';
        closeBtn.style.border = 'none';
        closeBtn.style.fontSize = '20px';
        closeBtn.style.cursor = 'pointer';
        closeBtn.style.padding = '0 8px';
        closeBtn.addEventListener('click', () => {
            panel.style.display = 'none';
        });

        header.appendChild(title);
        header.appendChild(closeBtn);

        // 内容区域
        const contentContainer = document.createElement('div');
        contentContainer.style.display = 'flex';
        contentContainer.style.flexDirection = 'column';
        contentContainer.style.flex = '1';
        contentContainer.style.overflow = 'hidden';

        // 精确匹配内容
        const exactContent = document.createElement('div');
        exactContent.style.padding = '16px';
        exactContent.style.overflowY = 'auto';
        exactContent.style.flex = '1';
        exactContent.style.display = 'block';

        // 正则匹配内容
        const regexContent = document.createElement('div');
        regexContent.style.padding = '16px';
        regexContent.style.overflowY = 'auto';
        regexContent.style.flex = '1';
        regexContent.style.display = 'none';

        // 添加新UP主的输入框
        const addExactContainer = document.createElement('div');
        addExactContainer.style.display = 'flex';
        addExactContainer.style.marginBottom = '16px';
        addExactContainer.style.gap = '8px';

        const exactInput = document.createElement('input');
        exactInput.type = 'text';
        exactInput.placeholder = '输入要屏蔽的UP主名称';
        exactInput.style.flex = '1';
        exactInput.style.padding = '8px';
        exactInput.style.border = '1px solid #ddd';
        exactInput.style.borderRadius = '4px';

        const addExactBtn = document.createElement('button');
        addExactBtn.textContent = '添加';
        addExactBtn.style.padding = '8px 16px';
        addExactBtn.style.background = '#fb7299'; // Bilibili pink color
        addExactBtn.style.color = '#fff';
        addExactBtn.style.border = 'none';
        addExactBtn.style.borderRadius = '4px';
        addExactBtn.style.cursor = 'pointer';
        addExactBtn.addEventListener('click', () => {
            const upName = exactInput.value.trim();
            if (upName && !exactBlacklist.includes(upName)) {
                exactBlacklist.push(upName);
                saveBlacklists();
                exactInput.value = '';
                updateExactList();
                BlockUp(); // Re-run blocking after update
            }
        });

        addExactContainer.appendChild(exactInput);
        addExactContainer.appendChild(addExactBtn);
        exactContent.appendChild(addExactContainer);

        // 添加正则表达式的输入框
        const addRegexContainer = document.createElement('div');
        addRegexContainer.style.display = 'flex';
        addRegexContainer.style.marginBottom = '16px';
        addRegexContainer.style.gap = '8px';

        const regexInput = document.createElement('input');
        regexInput.type = 'text';
        regexInput.placeholder = '输入正则表达式 (如: 小小.*Official)';
        regexInput.style.flex = '1';
        regexInput.style.padding = '8px';
        regexInput.style.border = '1px solid #ddd';
        regexInput.style.borderRadius = '4px';

        const addRegexBtn = document.createElement('button');
        addRegexBtn.textContent = '添加';
        addRegexBtn.style.padding = '8px 16px';
        addRegexBtn.style.background = '#fb7299';
        addRegexBtn.style.color = '#fff';
        addRegexBtn.style.border = 'none';
        addRegexBtn.style.borderRadius = '4px';
        addRegexBtn.style.cursor = 'pointer';
        addRegexBtn.addEventListener('click', () => {
            const regex = regexInput.value.trim();
            if (regex && !regexBlacklist.includes(regex)) {
                try {
                    new RegExp(regex); // Test if regex is valid
                    regexBlacklist.push(regex);
                    saveBlacklists();
                    regexInput.value = '';
                    updateRegexList();
                    BlockUp(); // Re-run blocking after update
                } catch (e) {
                    alert('无效的正则表达式: ' + e.message);
                }
            }
        });

        addRegexContainer.appendChild(regexInput);
        addRegexContainer.appendChild(addRegexBtn);
        regexContent.appendChild(addRegexContainer);

        // 精确匹配列表
        const exactList = document.createElement('ul');
        exactList.style.listStyle = 'none';
        exactList.style.padding = '0';
        exactList.style.margin = '0';

        // 正则匹配列表
        const regexList = document.createElement('ul');
        regexList.style.listStyle = 'none';
        regexList.style.padding = '0';
        regexList.style.margin = '0';

        // Update exact match list display
        function updateExactList() {
            exactList.innerHTML = '';
            exactBlacklist.forEach((upName, index) => {
                const item = document.createElement('li');
                item.style.display = 'flex';
                item.style.justifyContent = 'space-between';
                item.style.alignItems = 'center';
                item.style.padding = '8px 0';
                item.style.borderBottom = '1px solid #f1f2f3';

                const name = document.createElement('span');
                name.textContent = upName;
                name.style.flex = '1';

                const removeBtn = document.createElement('button');
                removeBtn.textContent = '移除';
                removeBtn.style.padding = '4px 8px';
                removeBtn.style.background = '#f56c6c'; // Red color
                removeBtn.style.color = '#fff';
                removeBtn.style.border = 'none';
                removeBtn.style.borderRadius = '4px';
                removeBtn.style.cursor = 'pointer';
                removeBtn.addEventListener('click', () => {
                    exactBlacklist.splice(index, 1);
                    saveBlacklists();
                    updateExactList();
                    BlockUp(); // Re-run blocking after update
                });

                item.appendChild(name);
                item.appendChild(removeBtn);
                exactList.appendChild(item);
            });

            // Show empty state if no items
            if (exactBlacklist.length === 0) {
                const empty = document.createElement('div');
                empty.textContent = '暂无精确匹配屏蔽UP主';
                empty.style.textAlign = 'center';
                empty.style.padding = '16px';
                empty.style.color = '#999';
                exactList.appendChild(empty);
            }
        }

        // Update regex match list display
        function updateRegexList() {
            regexList.innerHTML = '';
            regexBlacklist.forEach((regex, index) => {
                const item = document.createElement('li');
                item.style.display = 'flex';
                item.style.justifyContent = 'space-between';
                item.style.alignItems = 'center';
                item.style.padding = '8px 0';
                item.style.borderBottom = '1px solid #f1f2f3';

                const regexText = document.createElement('span');
                regexText.textContent = regex;
                regexText.style.flex = '1';
                regexText.style.fontFamily = 'monospace'; // Monospace font for regex

                const removeBtn = document.createElement('button');
                removeBtn.textContent = '移除';
                removeBtn.style.padding = '4px 8px';
                removeBtn.style.background = '#f56c6c';
                removeBtn.style.color = '#fff';
                removeBtn.style.border = 'none';
                removeBtn.style.borderRadius = '4px';
                removeBtn.style.cursor = 'pointer';
                removeBtn.addEventListener('click', () => {
                    regexBlacklist.splice(index, 1);
                    saveBlacklists();
                    updateRegexList();
                    BlockUp(); // Re-run blocking after update
                });

                item.appendChild(regexText);
                item.appendChild(removeBtn);
                regexList.appendChild(item);
            });

            // Show empty state if no items
            if (regexBlacklist.length === 0) {
                const empty = document.createElement('div');
                empty.textContent = '暂无正则匹配屏蔽规则';
                empty.style.textAlign = 'center';
                empty.style.padding = '16px';
                empty.style.color = '#999';
                regexList.appendChild(empty);
            }
        }

        // Initialize lists
        updateExactList();
        updateRegexList();

        exactContent.appendChild(exactList);
        regexContent.appendChild(regexList);

        contentContainer.appendChild(exactContent);
        contentContainer.appendChild(regexContent);

        panel.appendChild(tabContainer);
        panel.appendChild(header);
        panel.appendChild(contentContainer);

        // 选项卡切换
        exactTab.addEventListener('click', () => {
            exactTab.style.borderBottom = '2px solid #fb7299';
            regexTab.style.borderBottom = 'none';
            exactContent.style.display = 'block';
            regexContent.style.display = 'none';
        });

        regexTab.addEventListener('click', () => {
            regexTab.style.borderBottom = '2px solid #fb7299';
            exactTab.style.borderBottom = 'none';
            exactContent.style.display = 'none';
            regexContent.style.display = 'block';
        });

        document.body.appendChild(panel);
        return panel;
    }

    // Check if current page is a video page
    function isVideoPage() {
        if (window.location.pathname.startsWith('/video/')) {
            return true;
        } else {
            return false;
        }
    }

    // 在右侧导航栏添加黑名单管理按钮
    function addBlacklistManagerButton() {
        if (isVideoPage()) {
            //console.log('[Bilibili-BlackList] 视频页面不添加黑名单管理按钮');
            return;
        }
    
        const rightEntry = document.querySelector('.right-entry');
        if (!rightEntry || rightEntry.querySelector('#bilibili-blacklist-manager')) {
            //console.log('[Bilibili-BlackList] 黑名单管理按钮已存在或右侧导航栏不存在');
            return;
        }
        
        //console.log('[Bilibili-BlackList] 添加黑名单管理按钮');
        const li = document.createElement('li');
        li.id = 'bilibili-blacklist-manager';
        li.style.cursor = 'pointer';

        const btn = document.createElement('div');
        btn.className = 'right-entry-item';
        btn.style.display = 'flex';
        btn.style.flexDirection = 'column';
        btn.style.alignItems = 'center';
        btn.style.justifyContent = 'center';

        // Shield icon SVG
        const icon = document.createElement('div');
        icon.innerHTML = `
            <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
                <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
                <path d="M9 12l2 2 4-4"/>
            </svg>
        `;
        icon.style.color = '#fb7299'; // Bilibili pink color

        const text = document.createElement('div');

        btn.appendChild(icon);
        btn.appendChild(text);
        li.appendChild(btn);

        // Insert button in the navigation
        if (rightEntry.children.length > 1) {
            rightEntry.insertBefore(li, rightEntry.children[1]);
        } else {
            rightEntry.appendChild(li);
        }

        // Create panel if it doesn't exist
        let panel = document.getElementById('bilibili-blacklist-panel');
        if (!panel) {
            panel = createBlacklistPanel();
        }

        // Show panel when button is clicked
        li.addEventListener('click', () => {
            panel.style.display = 'flex';
        });
    }

    // 检查是否匹配黑名单
    function isBlacklisted(upName) {
        // 检查精确匹配
        if (exactBlacklist.includes(upName)) {
            return true;
        }

        // 检查正则匹配
        for (const regex of regexBlacklist) {
            try {
                const regExp = new RegExp(regex);
                if (regExp.test(upName)) {
                    return true;
                }
            } catch (e) {
                console.error(`[Bilibili-BlackList] 无效的正则表达式: ${regex}`, e);
            }
        }

        return false;
    }

    // 添加UP主到精确黑名单
    function addToExactBlacklist(upName) {
        if (!exactBlacklist.includes(upName)) {
            exactBlacklist.push(upName);
            saveBlacklists();
            //console.log(`[Bilibili-BlackList] 已添加 ${upName} 到精确黑名单`);
            BlockUp(); // Re-run blocking after update
        }
    }

    // 创建屏蔽按钮 (appears when hovering over video cards)
    function createBlockButton(upName) {
        const btn = document.createElement('div');
        btn.className = 'bilibili-blacklist-block-btn';
        btn.innerHTML = '×';
        btn.title = '屏蔽此UP主';
        
        // Styling for the block button
        btn.style.position = 'absolute';
        btn.style.top = '5px';
        btn.style.left = '5px';
        btn.style.width = '40px';
        btn.style.height = '20px';
        btn.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        btn.style.color = 'white';
        btn.style.borderRadius = '5%';
        btn.style.display = 'none';
        btn.style.justifyContent = 'center';
        btn.style.alignItems = 'center';
        btn.style.cursor = 'pointer';
        btn.style.zIndex = '100';
        btn.style.fontSize = '16px';
        btn.style.fontWeight = 'bold';
        btn.style.transition = 'opacity 0.2s';
        
        // Add to blacklist when clicked
        btn.addEventListener('click', (e) => {
            e.stopPropagation(); // Prevent event bubbling
            addToExactBlacklist(upName);
        });
        
        return btn;
    }

    // CSS selectors for finding UP names in different page layouts
    const selectors = [
        '.bili-video-card__info--owner span[title]', // Main page cards
        '.bili-video-card__text span[title]',       // Alternative card style
        '.video-page-card-small .upname',           // Small cards
    ];
    
    // 共用函数:查找UP主名称元素并提取名称
    function findUpNameElements(callback) {
        let foundElements = false;
        
        // Check all selectors for UP name elements
        selectors.forEach(selector => {
            document.querySelectorAll(selector).forEach(element => {
                foundElements = true;
                const title = element.getAttribute('title') || element.textContent.trim();
                const upName = title.split(' ')[0]; // Get first part of title (before space)
                callback(element, upName);
            });
        });
        
        return foundElements;
    }
    
    // 为每个视频卡片添加屏蔽按钮
    function addBlockButtons() {
        document.querySelectorAll('.bili-video-card').forEach(videoCard => {
            // Skip if button already exists
            if (videoCard.querySelector('.bilibili-blacklist-block-btn')) {
                return;
            }
            
            // Find the video cover element
            const cover = videoCard.querySelector('.bili-video-card__wrap') || videoCard.querySelector('.card-box');
            if (!cover) return;
            
            // Make sure cover has relative positioning
            if (window.getComputedStyle(cover).position === 'static') {
                cover.style.position = 'relative';
            }
            
            // 使用共用函数查找UP主名称
            findUpNameElements((element, upName) => {
                // Make sure we're working with the current video card
                if (element.closest('.bili-video-card') === videoCard) {
                    const btn = createBlockButton(upName);
                    cover.appendChild(btn);
                    
                    // Show button on hover
                    videoCard.addEventListener('mouseenter', () => {
                        btn.style.display = 'flex';
                        setTimeout(() => {
                            btn.style.opacity = '1';
                        }, 10);
                    });
                    
                    // Hide button when not hovering
                    videoCard.addEventListener('mouseleave', () => {
                        btn.style.opacity = '0';
                        setTimeout(() => {
                            btn.style.display = 'none';
                        }, 500);
                    });
                }
            });
        });
    }
    
    // Main function to block videos from blacklisted UP
    function BlockUp() {
        const foundElements = findUpNameElements((element, upName) => {
            if (isBlacklisted(upName)) {
                // Find the closest video container element
                const container = element.closest('.feed-card') || 
                                 element.closest('.bili-video-card') || 
                                 element.closest('.video-page-card-small') || 
                                 element.closest('.video-card');
                
                // Remove the entire video card if matched
                if (container) {
                    container.remove();
                    //console.log(`[Bilibili-BlackList] 已屏蔽: ${upName}`);
                }
            }
        });
    
        if (!foundElements) {
            //console.log('[Bilibili-BlackList] 警告: 未找到任何UP主元素');
        }
    }
    function BlockAD() {
        // 屏蔽某些推广
        document.querySelectorAll('.floor-single-card').forEach(adCard => {
            adCard.remove();
        });
        //屏蔽直播推广
        document.querySelectorAll('.bili-live-card').forEach(adCard => {
            adCard.remove();
        });
    }
    function BlockVideoPageAd() {
        // 屏蔽右上角推广
        document.querySelectorAll('.video-card-ad-small').forEach(adCard => {
            adCard.remove();
        });
        //大推广
        document.querySelectorAll('.slide-ad-exp').forEach(adCard => {
            adCard.remove();
        });
        //游戏推广
        document.querySelectorAll('.video-page-game-card-small').forEach(adCard => {
            adCard.remove();
        });
       
        
    }
    // 初始化观察者 (watches for DOM changes)
    function initObserver() {
        const rootNode = document.getElementById('i_cecream') || // Bilibili's main container IDs
                        document.getElementById('app') ||
                        document.documentElement;
        
        if (rootNode) {
            observer.observe(rootNode, {
                childList: true,  // Watch for added/removed nodes
                subtree: true     // Watch all descendants
            });
            //console.log('[Bilibili-BlackList] 监听已启动');
        } else {
            // Retry if root node not found
            setTimeout(initObserver, 500);
        }
    }

    // MutationObserver to detect new content loaded dynamically
    const observer = new MutationObserver((mutations) => {
        let shouldCheck = false;
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length > 0) {
                shouldCheck = true;
            }
        });
        
        // Run blocking after new content is added
        if (shouldCheck) {
            setTimeout(() => {
                BlockUp();
                BlockAD();
                addBlockButtons();
                addBlacklistManagerButton();
                BlockVideoPageAd();
            }, 1000);
        }
    });

    // Main initialization function
    function init() {
        BlockUp(); // Initial blocking
        initObserver(); // Start watching for changes
        addBlacklistManagerButton(); // Add management button
        BlockAD();
       
        // Also check when scrolling (for infinite scroll pages)
        if(!isVideoPage()) {
            window.addEventListener('scroll', () => {
                setTimeout(() => {
                    BlockUp();
                    addBlockButtons();
                    BlockAD();
                    //addBlacklistManagerButton();
                }, 1000);
            });
        }else
        {
            
        }
    }

    // Run when DOM is ready
    document.addEventListener('DOMContentLoaded', init);
    if (document.readyState === 'interactive' || document.readyState === 'complete') {
        init();
    }

    // 添加全局样式
    GM_addStyle(`
        /* Style for block button hover effect */
        .bili-video-card:hover .bilibili-blacklist-block-btn {
            display: flex !important;
            opacity: 1 !important;
        }
        /* Fix for video card layout */
        .bili-video-card__cover {
            contain: layout !important;
        }
        /* Ensure block button is clickable */
        .bilibili-blacklist-block-btn {
            pointer-events: auto !important;
        }
        /* Panel styling */
        #bilibili-blacklist-panel {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        }
        /* Button hover effects */
        #bilibili-blacklist-panel button {
            transition: background-color 0.2s;
        }
        #bilibili-blacklist-panel button:hover {
            opacity: 0.9;
        }
        /* Manager button hover effect */
        #bilibili-blacklist-manager:hover svg {
            transform: scale(1.1);
        }
        #bilibili-blacklist-manager svg {
            transition: transform 0.2s;
        }
        /* Input focus effect */
        #bilibili-blacklist-panel input:focus {
            outline: none;
            border-color: #fb7299 !important;
        }
    `);
})();