Greasy Fork

Greasy Fork is available in English.

高亮个别用户的弹幕

高亮个别用户的弹幕, 有时候找一些特殊人物(其他直播主出现在直播房间)用

当前为 2023-05-23 提交的版本,查看 最新版本

// ==UserScript==
// @name         高亮个别用户的弹幕
// @namespace    http://tampermonkey.net/
// @version      0.7.23
// @description  高亮个别用户的弹幕, 有时候找一些特殊人物(其他直播主出现在直播房间)用
// @author       Eric Lam
// @include      https://sc.chinaz.com/tag_yinxiao/tongzhi.html
// @include      /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/
// @include      /https?:\/\/eric2788\.github\.io\/scriptsettings\/highlight-user(\/)?/
// @include      /https?:\/\/eric2788\.neeemooo\.com\/scriptsettings\/highlight-user(\/)?/
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://cdn.jsdelivr.net/gh/google/brotli@5692e422da6af1e991f9182345d58df87866bc5e/js/decode.js
// @require      http://greasyfork.icu/scripts/417560-bliveproxy/code/bliveproxy.js?version=1045452
// @require      https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/toastr.js/2.1.4/toastr.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/build/md5.min.js
// @grant        GM.xmlHttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        unsafeWindow
// @run-at       document-start
// @connect      api.bilibili.com
// @website      https://eric2788.github.io/scriptsettings/highlight-user
// @homepage     https://eric2788.neeemooo.com/scriptsettings/highlight-user
// ==/UserScript==

(async function() {
    'use strict';

    const defaultSettings = {
        highlightUsers: [
            396024008, // 日本兄贵
            604890122, // 日本兄贵
            623441609, // 凤玲天天 (DD)
            1618670884, // 日本兄贵
            406805563, // 乙女音
            2299184, // 古守
            198297, // 冰糖
            1576121 // paryi
        ],
        settings: {
            color: '#FFFF00',
            opacity: 1.0,
            playAudio: false,
            playAudioDanmu: false,
            join_notify_duration: 5000,
            join_notify_position: "bottom-left",
            volume: {
                danmu: 1.0,
                join: 1.0
            }
        }
    }

    const defaultSounds = {
        join: '//downsc.chinaz.net/Files/DownLoad/sound1/201911/12221.mp3',
        danmu: '//downsc.chinaz.net/Files/DownLoad/sound1/202003/12643.mp3'
    }

    const storage = GM_getValue('settings', defaultSettings)
    const sounds = GM_getValue('sounds', defaultSounds)
    const { highlightUsers, settings: currentSettings } = storage
    const settings = { ...defaultSettings.settings, ...currentSettings }
    console.debug(highlightUsers)
    console.debug(settings)

    // gener w_rid
    /* reference
    def w_rid():  # 每次请求生成w_rid参数
      wts = str(int(time.time()))  # 时间戳
      c = "72136226c6a73669787ee4fd02a74c27"  # 尾部固定值,根据imgKey,subKey计算得出
      b = "mid=" + uid + "&platform=web&token=&web_location=1550101"
      a = b + "&wts=" + wts + c  # mid + platform + token + web_location + 时间戳wts + 一个固定值
      return hashlib.md5(a.encode(encoding='utf-8')).hexdigest()
    */
    function w_rid(uid) {
       const wts = `${Date.now()}`
       const c = "72136226c6a73669787ee4fd02a74c27"
       const b =  "mid=" + uid + "&platform=web&token=&web_location=1550101"
       const a = b + "&wts=" + wts + c  // mid + platform + token + web_location + 时间戳wts + 一个固定值
       const m = md5.create()
       m.update(a)
       return m.hex()
    }

    async function requestUserInfo(mid) {
        let error = null;
        const baseUrls = [
            `https://api.bilibili.com/x/space/acc/info?mid=${mid}&jsonp=jsonp`, // 已經失效
            `https://api.bilibili.com/x/space/wbi/acc/info?mid=${mid}&jsonp=jsonp`, // 已經失效
            `https://api.bilibili.com/x/space/wbi/acc/info?platform=web&token=&web_location=1550101&wts=${Date.now()}&mid=${mid}&w_rid=${w_rid(mid)}`
        ]
        for(const base of baseUrls) {
            try {
                return await webRequest(base)
            }catch(err){
                console.error(`使用 ${base} 請求時出現錯誤: ${err?.message ?? err}`);
                console.warn(`嘗試使用下一個API`)
                error = err;
            }
        }
        console.warn('沒有可以使用的下一個API,將拋出錯誤')
        throw error;
    }

    if (location.origin == 'https://live.bilibili.com'){
        console.log('using highlight filter')

        function hexToNum(color){
            const hex = color.substr(1)
            return parseInt(hex, 16)
        }

        $(document.head).append(`<link href="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/toastr.js/2.1.4/toastr.min.css" rel="stylesheet" />`)

        const audio = {
            join: new Audio(sounds.join),
            danmu: new Audio(sounds.danmu)
        }
        audio.join.volume = settings.volume.join
        audio.danmu.volume = settings.volume.danmu
        const highlights = new Set()
        const highlightsMapper = new Map()
        toastr.options = {
            "closeButton": false,
            "debug": false,
            "newestOnTop": true,
            "progressBar": true,
            "positionClass": `toast-${settings.join_notify_position}`,
            "preventDuplicates": false,
            "onclick": null,
            "showDuration": "300",
            "hideDuration": "1000",
            "timeOut": `${settings.join_notify_duration}`,
            "extendedTimeOut": "1000",
            "showEasing": "swing",
            "hideEasing": "linear",
            "showMethod": "fadeIn",
            "hideMethod": "fadeOut"
        }

        const elements = ['.danmaku-item-container']

        async function launch(){
            console.debug('launching highlight filter...')
            while(!unsafeWindow.bliveproxy){
                console.log('cannot not find bliveproxy, wait one second')
                await sleep(1000)
            }
            while(!elements.some(s => $(s).length > 0)){
                console.log('cannot not find element, wait one second')
                await sleep(1000)
            }

            function handleUserEnter(uid, uname){
                console.debug(`user enter: ${uid} (${uname})`)
                if (!highlightUsers.includes(uid)) return
                console.log(`name: ${uname} has enter this live room`)
                toastr.info(`你所关注的用户 ${uname} 已进入此直播间。`, `噔噔咚!`)
                if (settings.playAudio) audio.join.play()
            }

            console.debug('bliveproxy injected.')
            unsafeWindow.bliveproxy.addCommandHandler('DANMU_MSG', command => {
                const userId = command.info[2][0]
                console.debug(`user send danmu: ${userId}`)
                if (!highlightUsers.includes(userId)) return
                console.debug('detected highlighted user: '+userId)

                /* 新版直播间无法改写弹幕信息 👇
                command.info[0][13] = "{}" // 把那些圖片彈幕打回原形
                if (settings.color) {
                    command.info[0][3] = hexToNum(settings.color)
                }
                command.info[1] += `(${command.info[2][1]})`
                console.debug(`converted danmaku: ${command.info[1]}`)]
                highlights.add(command.info[1])

                */
                highlightsMapper.set(command.info[1], command.info[2][1]);
                if (settings.playAudioDanmu) audio.danmu.play()
            })
            unsafeWindow.bliveproxy.addCommandHandler('INTERACT_WORD', ({data}) => {
                const {uid, uname} = data
                handleUserEnter(uid, uname)
            })
            unsafeWindow.bliveproxy.addCommandHandler('ENTRY_EFFECT', async ({data}) => {
                const { uid } = data
                if (!highlightUsers.includes(uid)) return
                let username;
                try {
                    const cache = GM_getValue(uid, null)
                    if (cache != null && cache.name != `无法索取用户资讯`){
                        username = cache.name
                    }else{
                        const { name } = await requestUserInfo(uid)
                        username = name
                    }
                    console.debug(`成功辨别舰长 ${uid} 名称为 ${name}`)
                }catch(err){
                    console.error(`索取大航海用户资讯错误: ${err}`)
                    console.warn(`将使用 uid 作为名称`)
                    username = `(UID: ${uid})`
                }
                handleUserEnter(uid, username)
            })
            if (settings.opacity){
                const config = { attributes: false, childList: true, subtree: true }
                function danmakuCheckCallback(mutationsList){
                    for(const mu of mutationsList){
                        for (const node of mu.addedNodes){
                            console.log('node', node);
                            const danmaku = node?.innerText?.trim() ?? node?.data?.trim()
                            console.log('danmaku', danmaku)
                            if (danmaku === undefined || danmaku === '') continue
                            //if (!highlights.has(danmaku)) continue
                            if (!highlightsMapper.has(danmaku)) continue;
                            const user = highlightsMapper.get(danmaku);
                            console.debug('highlighting danmaku: ', danmaku, ' with user: ', user)
                            const n = node.innerText !== undefined ? node : node.parentElement
                            const jimaku = $(n)
                            jimaku.css('opacity', `${settings.opacity}`)
                            jimaku.css('color', `${settings.color}`)
                            jimaku.text(`${danmaku}(${user})`);
                            highlights.delete(danmaku)
                            highlightsMapper.delete(danmaku)
                        }
                    }
                }
                const danmakuObserver = new MutationObserver((mu, obs) => danmakuCheckCallback(mu))
                danmakuObserver.observe($('.danmaku-item-container')[0], config)
            }
        }

        await launch()
    } else if (["https://eric2788.github.io", "https://eric2788.neeemooo.com", "http://127.0.0.1:5500"].includes(location.origin)){
        while(!unsafeWindow.mdui){
            console.debug('cannot find mdui, wait one second')
            await sleep(1000)
        }
        const $ = mdui.$
        async function appendUser(userId){
            if ($(`#${userId}`).length > 0){
                mdui.alert('该用户已在列表内')
                return false
            }
            try {
                const lastUpdate = GM_getValue('last.update', new Date())
                const haveData = GM_getValue(userId, null) != null
                const today = new Date()
                if (!haveData || Math.abs(today - lastUpdate) > (86400 * 1000 * 7)) {
                    console.log('cache outdated, updating user info...')
                    const { name, face } = await requestUserInfo(userId)
                    GM_setValue(userId, {name, face})
                    GM_setValue('last.update', new Date())
                    console.log('user info updated and saved to cache.')
                }else{
                    console.log('loading user info from cache.')
                }
                const {name, face} = GM_getValue(userId, {name: `无法索取用户资讯`, face: ''})
                $('#hightlight-users').append(`
                    <label class="mdui-list-item mdui-ripple">
                        <div class="mdui-checkbox">
                            <input type="checkbox" id="${userId}"/>
                            <i class="mdui-checkbox-icon"></i>
                        </div>
                        <div class="mdui-list-item-avatar"><img src="${face}"/></div>
                        <div class="mdui-list-item-content">${name} (${userId})</div>
                   </label>
                `)
                return true;
            }catch(err){
                console.warn(err)
                if (err.code == -412){
                    const {name, face} = GM_getValue(userId, {name: `无法索取用户资讯`, face: ''})
                    $('#hightlight-users').append(`
                    <label class="mdui-list-item mdui-ripple">
                        <div class="mdui-checkbox">
                            <input type="checkbox" id="${userId}"/>
                            <i class="mdui-checkbox-icon"></i>
                        </div>
                        <div class="mdui-list-item-avatar"><img src="${face}"/></div>
                        <div class="mdui-list-item-content">${name} (${userId})</div>
                   </label>
                  `)
                    return true;
                }else{
                    mdui.alert(`无法索取 ${userId} 的用户资讯: ${err.message}`)
                    return false;
                }
            }finally{
                $(`#${userId}`).on('change', e => {
                    if (getTicked().length > 0) {
                        $('#delete-btn').show()
                    } else {
                        $('#delete-btn').hide()
                    }
                })
            }
        }


        function getTicked() {
            return $('#hightlight-users').find('.mdui-checkbox > input').filter((i, e) => $(e).prop('checked')).map((i, e) => $(e).attr('id'))
        }

        $('#delete-btn').on('click', e => {
            getTicked().each((i, id) => $(`#${id}`).parents('.mdui-list-item').remove())
            GM_setValue('settings', getSettings())
            mdui.snackbar('删除并保存成功')
            $('#delete-btn').hide()
        })

        $('#user-add').on('keypress', async (e) => {
            if (e.which != 13) return
            if (!$('#user-add')[0].checkValidity()) return
            if (await appendUser(e.target.value)){
                GM_setValue('settings', getSettings())
                mdui.snackbar('新增并保存成功')
                e.target.value = ''
            }
        });

        $('#save-btn').on('click', e => {
            if (!$('form')[0].checkValidity()){
                mdui.snackbar('保存失败,请检查格式或漏填')
                return
            }
            GM_setValue('settings', getSettings())
            mdui.snackbar('保存成功')
        })

        $('#try-listen-join').on('click', () => {
            const audio = new Audio(sounds.join)
            audio.volume = parseVolume('#volume-join')
            $('#try-listen-join').attr('disabled', '')
            audio.addEventListener('canplaythrough', () => {
                audio.play()
                $('#try-listen-join').removeAttr('disabled')
            })
        })

        $('#try-listen-danmu').on('click', () => {
            const audio = new Audio(sounds.danmu)
            audio.volume = parseVolume('#volume-danmu')
            $('#try-listen-danmu').attr('disabled', '')
            audio.addEventListener('canplaythrough', () => {
                audio.play()
                $('#try-listen-danmu').removeAttr('disabled')
            })
        })

        const joinNotifyPosSelect = new mdui.Select('#join-notify-position', {position: 'bottom'})


        $('#import-setting').on('click', async () => {
            try {
                const area = $('#setting-area').val()
                const {highlightUsers, settings: currentSettings } = JSON.parse(area)
                const settings = { ...defaultSettings.settings, ...currentSettings }
                $('.mdui-list-item').remove() // clear old data
                await initializeSettings({highlightUsers, settings})
                mdui.snackbar('设定档导入成功,请记得按下保存')
                $('#setting-area').val('')
            }catch(err){
                console.error(err)
                mdui.snackbar('设定档导入失败,请检查格式有没有错误')
            }
        })

        $('#export-setting').on('click', () => {
            const area = JSON.stringify(getSettings())
            $('#setting-area').val(area)
            const text = $('#setting-area')[0]
            text.select();
            text.setSelectionRange(0, 99999);
            document.execCommand("copy")
            mdui.snackbar('设定档已导出并复制成功')
            $('#setting-area').val('')
        })

        async function initializeSettings({highlightUsers, settings}){
            await Promise.all(highlightUsers.map((id) => appendUser(id)))
            $('#opacity')[0].valueAsNumber = settings.opacity
            $('#color').val(settings.color)
            $('#color-picker').val(settings.color)
            $('#color-picker-btn').css('color', settings.color)
            $('#play-audio').prop('checked', settings.playAudio)
            $('#play-audio-danmu').prop('checked', settings.playAudioDanmu)
            $('#join-notify-duration')[0].valueAsNumber = settings.join_notify_duration
            $('#join-notify-position').val(settings.join_notify_position)
            $('#volume-danmu').val(settings.volume.danmu * 100)
            $('#volume-join').val(settings.volume.join * 100)
            mdui.updateSliders()
            joinNotifyPosSelect.handleUpdate()
            $('#list-loading').hide()
        }

        await initializeSettings({highlightUsers, settings})

        function getSettings(){
            const users = new Set()
            $('#hightlight-users').find('.mdui-checkbox > input').map((i, e) => parseInt($(e).attr('id'))).filter((i,e) => !!e).each((i,e) => users.add(e))
            const settings = {
                opacity: $('#opacity')[0].valueAsNumber,
                color: $('#color')[0].checkValidity() ? $('#color').val() : '',
                playAudio: $('#play-audio').prop('checked'),
                playAudioDanmu: $('#play-audio-danmu').prop('checked'),
                join_notify_duration: $('#join-notify-duration')[0].valueAsNumber,
                join_notify_position: $('#join-notify-position').val(),
                volume: {
                    danmu: parseVolume('#volume-danmu'),
                    join: parseVolume('#volume-join')
                }
            }
            return { highlightUsers: [...users], settings }
        }

        function parseVolume(element){
            const val = $(element)[0].value
            if (val == 0) return 0.0
            return parseFloat((val / 100).toFixed(2)) || 1.0
        }

    } else if (location.origin === 'https://sc.chinaz.com'){
        while ($('div.audio-class').length == 0){
            await sleep(1000)
        }
        $('div.audio-class').empty();
        $('div.audio-class')
            .append(`<a href="javascript: void(0)" id="danmu-select">选为弹幕通知</a>`)
            .append('<a href="javascript: void(0)" id="join-select">选为进入通知</a>')
        $('a#danmu-select').on('click', e => {
            e.preventDefault();
            if (!window.confirm('确定选择为弹幕通知音效?')) return
            const url = $(e.target).parents('.audio-item').children('audio').attr('src')
            if (!url) {
                alert('选择失败,无效的URL')
                return
            }
            sounds.danmu = url
            GM_setValue('sounds', sounds)
            alert('设置成功')
        })

        $('a#join-select').on('click', e => {
            e.preventDefault();
            if (!window.confirm('确定选择为进入通知音效?')) return
            const url = $(e.target).parents('.audio-item').children('audio').attr('src')
            if (!url) {
                alert('选择失败,无效的URL')
                return
            }
            sounds.join = url
            GM_setValue('sounds', sounds)
            alert('设置成功')
        })
    }
})().catch(console.error);

async function webRequest(url){
    const data = await GM.xmlHttpRequest({
        method: "GET",
        headers: {
            'Content-type' : 'application/json',
            'Referer': 'https://www.bilibili.com',
            'Origin': 'https://www.bilibili.com'
        },
        url
    })
    console.log(`response for ${url}: ${data?.response ?? data}`);
    const res = JSON.parse(data.response)
    if (res.code !== 0) throw res
    return res.data
}

async function sleep(ms){
    return new Promise((res,) => setTimeout(res,ms))
}