哔哩哔哩直播间屏蔽工具

哔哩哔哩直播间屏蔽工具,支持管理列表,批量屏蔽,导出、导入列表等……

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              Bilibili Liveroom Filter
// @name:zh-CN        哔哩哔哩直播间屏蔽工具
// @description       Filtering Bilibili liveroom, batch management, export, import banlist...
// @description:zh-CN 哔哩哔哩直播间屏蔽工具,支持管理列表,批量屏蔽,导出、导入列表等……
// @author            jc3213
// @namespace         https://github.com/jc3213/userscript
// @supportURL        https://github.com/jc3213/userscript/issues
// @homepageURL       https://github.com/jc3213/userscript
// @license           MIT
// @match             https://live.bilibili.com/*
// @grant             GM_getValue
// @grant             GM_setValue
// @noframes          
// @icon              https://i0.hdslb.com/bfs/static/jinkela/long/images/512.png
// @compatible        chrome
// @compatible        firefox
// @compatible        edge
// @compatible        opera
// @compatible        safari
// @compatible        kiwi
// @compatible        qq
// @compatible        via
// @compatible        brave
// @version           2025.6.2.1
// ==/UserScript==

'use strict'
let storage = GM_getValue('storage', { every: [] })
let showRooms = { every: [] }
let firstRun = true
let bilicss = document.createElement('style')
bilicss.textContent = '.bililive-button {background-color: #00ADEB; border-radius: 5px; color: #ffffff; cursor: pointer; font-size: 16px; padding: 3px 10px; user-select: none; text-align: center;} .bililive-button:hover {filter: contrast(75%);} .bililive-button:active {filter: contrast(45%);} '

let area = location.pathname.slice(1)
if (isNaN(area)) {
    biliLiveSpecialArea()
}
else {
    PromiseSelector('.header-info-ctnr > .rows-content').then((liver) => biliLiveShowRoom(liver, area)).catch((error) => biliLiveShowFrame(area))
}

async function biliLiveSpecialArea() {
    let area = await PromiseSelector('#room-card-list')
    biliLiveManagerDeployed(area)
    document.querySelectorAll('.index_item_JSGkw').forEach(biliLiveShowCover)
    let observer = new MutationObserver((mutationsList) => {
        mutationsList.forEach((mutation) => {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach((node) => {
                    if (node.tagName === 'DIV' && node.className === 'index_item_JSGkw') {
                        biliLiveShowCover(node)
                    }
                })
            }
        })
    })
    observer.observe(area, { childList: true, subtree: true })
}

const balloonHandlers = {
    'bililive-block': (id, liver) => {
        event.preventDefault()
        if (confirm('确定要永久屏蔽【 ' + liver + ' 】的直播间吗?')) {
            blockLiveRoom(id, liver)
            GM_setValue('storage', storage)
        }
    },
    'bililive-image': (id, liver, title, image) => {
        event.preventDefault()
        if (confirm('确定要打开直播《 ' + title + ' 》的封面吗?')) {
            open(image, '_blank')
        }
    }
}

async function biliLiveShowCover(node) {
    if (node.cover) {
        return
    }

    let pane = node.children[0]
    let room = pane.href
    let id = room.slice(room.lastIndexOf('/') + 1, room.indexOf('?'))
    let [top, center] = pane.children[1].children
    let thumb = top.children[0].style['background-image']
    let image = 'https' + thumb.slice(thumb.indexOf(':'), thumb.lastIndexOf('@'))
    let [name, user] = center.children[1].children
    let title = name.textContent.trim()
    let liver = user.children[0].textContent.trim()

    let menu = document.createElement('div')
    menu.className = 'bililive-balloon'
    menu.innerHTML = '<div id="bililive-block" class="bililive-button">屏蔽直播间</div><div id="bililive-image" class="bililive-button">查看封面图</div></div'
    menu.addEventListener('click', (event) => {
        let handler = balloonHandlers[event.target.id]
        if (handler) {
            event.preventDefault()
            handler(id, liver, title, image)
        }
    })

    showRooms[id] = node
    showRooms.every.push(node)

    center.after(menu)
    node.cover = true
    node.style.display = storage[id] ? 'none' : ''
    node.addEventListener('mouseover', (event) => { menu.style.display = 'flex' })
    node.addEventListener('mouseout', (event) => { menu.style.display = '' })
}

async function biliLiveShowRoom(menu, id, xid) {
    let [upper, lower] = menu.children
    let left = upper.children[0]
    let liver = left.children[0].textContent.trim()
    let area = lower.children[0].children[1].children[0].href

    if (storage[id] && !confirm('【 ' + liver + ' 】的直播间已被屏蔽,是否继续观看?')) {
        open(area, '_self')
    }

    let block = document.createElement('div')
    block.textContent = '屏蔽直播间'
    block.className = 'bililive-button'
    block.addEventListener('click', (event) => {
        if (confirm('确定要永久屏蔽【 ' + liver + ' 】的直播间吗?')) {
            blockLiveRoom(id, liver)
            if (xid) {
                blockLiveRoom(xid, liver)
            }
            GM_setValue('storage', storage)
            open(area, '_self')
        }
    })

    bilicss.textContent += '.bililive-button {margin-left: 10px;}'
    left.append(block, bilicss)
}

async function biliLiveShowFrame(id) {
    let iframe = await PromiseSelector('iframe[src*="live.bilibili.com"]')
    let menu = await PromiseSelector('.rows-ctnr.rows-content', iframe.contentDocument)
    let xid = iframe.src.match(/\/(\d+)/)[1]
    biliLiveShowRoom(menu, id, xid)
}

function addToFilterList(id, liver) {
    let cell = document.createElement('div')
    cell.innerHTML = '<div></div><div></div>'

    let [room, user] = cell.children

    user.textContent = liver
    room.textContent = id
    room.addEventListener('click', (event) => {
        if (storage[id] && confirm('确定要解除对【 ' + liver + ' 】的屏蔽吗?')) {
            cell.remove()
            let index = storage.every.findIndex((i) => i === id)
            storage.every.splice(index, 1)
            delete storage[id]
            GM_setValue('storage', storage)
            unblockLiveRoom(id)
        }
    })

    showRooms.table.appendChild(cell)
}

function blockLiveRoom(id, liver) {
    if (!storage[id]) {
        storage.every.push(id)
        storage[id] = liver
        let room = showRooms[id]
        if (room) {
            room.style.display = 'none'
        }
        if (!firstRun) {
            addToFilterList(id, liver)
        }
    }
}

function unblockLiveRoom(id) {
    let room = showRooms[id]
    if (room) {
        room.style.display = ''
    }
}

function biliLiveManagerDeployed(area) {
    let pane = document.createElement('div')
    pane.className = 'bililive-container'
    pane.innerHTML = `
<div class="bililive-button">管理列表</div>
<div class="bililive-manager">
    <div id="bililive-block" class="bililive-button">批量屏蔽</div>
    <div class="bililive-button"><label for="bililive-import">bili导入列表</label></div>
    <div id="bililive-export" class="bililive-button">导出列表</div>
    <div id="bililive-clear" class="bililive-button">清空列表</div>
    <textarea rows="6"></textarea>
    <div class="bililive-table bililive-thead">
        <div>直播间ID</div>
        <div>主播昵称</div>
    </div>
    <div class="bililive-table bililive-tbody"></div>
</div>
<input id="bililive-import" type="file" accept=".json">
<a></a>
`

    let [menu, popup, upload, saver] = pane.children
    let [batch, , fileDl, clear, entry, thead, tbody] = popup.children

    upload.addEventListener('change', async (event) => {
        let file = upload.files[0]
        if (confirm('确定要导入屏蔽列表【' + file.name.slice(0, -5) + '】吗?')) {
            let json = await PromiseFileReader(file)
            json.forEach(({ id, liver }) => blockLiveRoom(id, liver))
            GM_setValue('storage', storage)
            upload.value = ''
        }
    })

    batch.addEventListener('click', (event) => {
        if (confirm('确定要屏蔽列表中的直播间吗?')) {
            entry.value.match(/[^\r\n]+/g)?.forEach((str) => {
                var rule = str.match(/(\d+)[\\/:*?"<>|[\](){}+\-`,.;!@#%^&]+(.+)/)
                if (rule?.length === 3) {
                    blockLiveRoom(rule[1], rule[2])
                }
            })
            GM_setValue('storage', storage)
            entry.value = ''
        }
    })

    fileDl.addEventListener('click', (event) => {
        if (confirm('确定要导出当前屏蔽列表吗?')) {
            let output = []
            storage.every.forEach((id) => output.push({ id, liver: storage[id] }))
            let blob = new Blob([JSON.stringify(output, null, 4)], { type: 'application/json' })
            saver.href = URL.createObjectURL(blob)
            saver.download = 'bilibili直播间屏蔽列表'
            saver.click()
        }
    })

    clear.addEventListener('click', (event) => {
        if (confirm('确定要清空当前屏蔽列表吗?')) {
            storage.every.forEach(unblockLiveRoom)
            storage = { every: [] }
            GM_setValue('storage', storage)
            tbody.innerHTML = ''
        }
    })

    menu.addEventListener('click', (event) => {
        if (firstRun) {
            storage.every.forEach((id) => addToFilterList(id, storage[id]))
            firstRun = false
        }
        popup.classList.toggle('bililive-popup')
    })

    showRooms.table = tbody

    document.getElementsByClassName('tabs')[0].appendChild(pane)

    bilicss.textContent += `.bililive-button {flex: 1;}
.bililive-balloon {display: none; gap: 5px; margin: 8px 12px 0px 6px;}
.bililive-container {position: relative;}
.bililive-container > input, .bililive-container > a {display: none;}
.bililive-manager {background-color: #ffffff; border: 1px solid #000000; display: none; font-size: 16px; padding: 5px; margin-top: 3px; position: absolute; width: 520px; z-index: 3213;}
.bililive-manager > textarea {font-size: 16px; margin: 3px 0px; padding: 5px; resize: none;}
.bililive-manager > textarea, .bililive-manager > .bililive-table {flex-basis: 100%;}
.bililive-popup {display: flex; gap: 3px; flex-wrap: wrap;}
.bililive-thead, .bililive-tbody > div {display: flex;}
.bililive-thead > *, .bililive-tbody > div > * {border: 1px solid #ffffff; flex: 1; padding: 5px; text-align: center; user-select: text !important;}
.bililive-thead > * {background-color: #000000; color: #ffffff;}
.bililive-tbody {height: 480px; scroll-y: auto; border: 1px solid #000000;}
.bililive-tbody > * > :first-child {background-color: #FF6699; color: #ffffff; cursor: pointer;}
.bililive-tbody > * > :first-child:active {contrast(45%);}
.bililive-tbody > :nth-child(2n) > :last-child {background-color: #E2E3E4;}
.bililive-tbody > :nth-child(2n + 1) > :last-child {background-color: #F1F2F3;}
`
    area.append(bilicss)
}

function PromiseFileReader(file) {
    return new Promise((resolve, reject) => {
        let reader = new FileReader()
        reader.readAsText(file)
        reader.onload = () => resolve(JSON.parse(reader.result))
    })
}

function PromiseSelector(selector, anchor = document) {
    return new Promise((resolve, reject) => {
        let quota = 15
        let timer = setInterval(() => {
            let node = anchor.querySelector(selector)
            if (node) {
                clearInterval(timer)
                resolve(node)
            }
            if (--quota === 0) {
                clearInterval(timer)
                reject()
            }
        }, 200)
    })
}