收收味

B站直播弹幕清理,旨在提高弹幕的信息密度

// ==UserScript==
// @name         收收味
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  B站直播弹幕清理,旨在提高弹幕的信息密度
// @author       xfgryujk
// @include      /https?:\/\/live\.bilibili\.com\/?\??.*/
// @include      /https?:\/\/live\.bilibili\.com\/\d+\??.*/
// @include      /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/
// @run-at       document-start
// @require      https://cdn.jsdelivr.net/gh/google/brotli@5692e422da6af1e991f9182345d58df87866bc5e/js/decode.js
// @require      https://gf.qytechs.cn/scripts/417560-bliveproxy/code/bliveproxy.js?version=984333
// @grant        none
// ==/UserScript==

(function() {
  let filterers = []

  function main() {
    filterers = [
      trim,
      shink1CharLeft,
      shink1CharRight,
      shink2CharLeft,
      shink3CharLeft,

      hideRecentRepeat,
      hideRepeater,
    ]
    bliveproxy.addCommandHandler('DANMU_MSG', danmakuHandler)
  }

  function danmakuHandler(command) {
    let ctx = new DanmakuFilterContext(command)
    for (let filterer of filterers) {
      filterer(ctx)
      if (!ctx.showDanmaku && !ctx.showComment) {
        break
      }
    }

    let info = command.info

    if (info[1] != ctx.msg) {
      // console.log(`${info[1]} -> ${ctx.msg}`)
      info[1] = ctx.msg
    }

    let hideFlag = 0
    if (!ctx.showComment) {
      hideFlag |= 1
    }
    if (!ctx.showDanmaku) {
      hideFlag |= 2
    }
    if (hideFlag != 0) {
      // console.log(`hide: ${ctx.msg}`)
      let params = command.cmd.split(':')
      while (params.length < 5) {
        params.push('0')
      }
      params[4] = hideFlag.toString()
      command.cmd = params.join(':')
    }
  }

  class DanmakuFilterContext {
    constructor(command) {
      let info = command.info
      this.msg = info[1]
      this.uid = info[2][0]
      this.username = info[2][1]

      this._showDanmaku = true
      this._showComment = true
    }

    get showDanmaku() {
      return this._showDanmaku
    }

    hideDanmaku() {
      this._showDanmaku = false
    }

    get showComment() {
      return this._showComment
    }

    hideComment() {
      this._showComment = false
    }
  }

  function trim(ctx) {
    ctx.msg = ctx.msg.trim()
  }

  function shink1CharLeft(ctx) {
    // 以至少4个相同字符开头,尾部可跟最多4个任意字符,例:888888888
    const REG = /^(.)\1{3,}.{0,4}?$/
    let match = REG.exec(ctx.msg)
    if (match !== null) {
      // 保留3个字符
      ctx.msg = [match[1], match[1], match[1]].join('')
    }
  }

  function shink1CharRight(ctx) {
    // 以最多4个任意字符开头,尾部至少4个相同字符,例:ohhhhhhhh
    const REG = /^(.{0,4}?)(.)\2{3,}$/
    let match = REG.exec(ctx.msg)
    if (match !== null) {
      // 保留头部+尾部3个字符
      ctx.msg = [match[1], match[2], match[2], match[2]].join('')
    }
  }

  function shink2CharLeft(ctx) {
    // 以2个字符至少重复3次开头,例:欧啦欧啦欧啦欧啦
    const REG = /^(..)\1\1/
    let match = REG.exec(ctx.msg)
    if (match !== null) {
      // 保留2次重复
      ctx.msg = match[1] + match[1]
    }
  }

  function shink3CharLeft(ctx) {
    // 以至少3个字符至少重复2次开头,例:awslawslawslawsl
    const REG = /^(.{3,}?)\1/
    let match = REG.exec(ctx.msg)
    if (match !== null) {
      // 保留1次重复
      ctx.msg = match[1]
    }
  }

  // msg -> {time: 上次显示弹幕时间}
  let recentRepeatInfoMap = new Map()
  const MIN_REPEAT_INTERVAL = 10 * 1000
  // 隐藏最近重复的弹幕
  function hideRecentRepeat(ctx) {
    let curTime = new Date()
    let key = ctx.msg.toLowerCase()
    let repeatInfo = recentRepeatInfoMap.get(key)

    if (repeatInfo === undefined) {
      repeatInfo = {time: curTime}
      recentRepeatInfoMap.set(key, repeatInfo)

      // 清理
      if (recentRepeatInfoMap.size >= 1024) {
        let keysToDel = []
        for (let [key, repeatInfo] of recentRepeatInfoMap.entries()) {
          if (curTime - repeatInfo >= MIN_REPEAT_INTERVAL) {
            keysToDel.push(key)
          }
        }
        for (let key of keysToDel) {
          recentRepeatInfoMap.delete(key)
        }
      }
      return
    }
    if (curTime - repeatInfo.time >= MIN_REPEAT_INTERVAL) {
      repeatInfo.time = curTime
      return
    }
    ctx.hideDanmaku()
  }

  // uid -> {msg: 重复弹幕内容, startTime: 重复开始时间, repeatNum: 重复次数}
  let uidRepeatInfoMap = new Map()
  let uidBlacklistForRepeat = new Set()
  const REPEAT_PERIOD_PER_USER = 30 * 1000
  const REPEAT_NUM_FOR_ADD_BLACK = 3
  // 屏蔽复读机用户
  function hideRepeater(ctx) {
    if (uidBlacklistForRepeat.has(ctx.uid)) {
      ctx.hideDanmaku()
      ctx.hideComment()
      return
    }

    let curTime = new Date()
    let repeatInfo = uidRepeatInfoMap.get(ctx.uid)

    if (repeatInfo !== undefined) {
      // 清理
      if (curTime - repeatInfo.startTime > REPEAT_PERIOD_PER_USER || repeatInfo.msg != ctx.msg) {
        repeatInfo = undefined
      }
    }

    if (repeatInfo === undefined) {
      // 第一次重复计数
      repeatInfo = {
        msg: ctx.msg,
        startTime: curTime,
        repeatNum: 1
      }
      uidRepeatInfoMap.set(ctx.uid, repeatInfo)
      return
    }

    if (++repeatInfo.repeatNum >= REPEAT_NUM_FOR_ADD_BLACK) {
      console.log(`拉黑【${ctx.username}】,uid=${ctx.uid},复读内容:${repeatInfo.msg}`)
      ctx.msg = '【此用户已屏蔽】' + ctx.msg
      uidBlacklistForRepeat.add(ctx.uid)
      ctx.hideDanmaku()
    }
  }

  main()
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址