Greasy Fork is available in English.

哔哩哔哩(B站, bilibili)播放界面和部分操作优化

B站播放器速度自定义(0.25 ~ 3), 支持快捷键(z:正常, x:减少速度, c:增加速度), 鼠标中键切换全屏等

Fra og med 10.08.2021. Se Den nyeste version.

// ==UserScript==
// @name         哔哩哔哩(B站, bilibili)播放界面和部分操作优化
// @description  B站播放器速度自定义(0.25 ~ 3), 支持快捷键(z:正常, x:减少速度, c:增加速度), 鼠标中键切换全屏等
// @namespace    bili
// @version      1.6.17
// @author       vizo
// @require      https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js
// @include      *bilibili.com/video/*
// @include      *bilibili.com/bangumi/*
// @include      *bilibili.com/medialist/*
// @include      *bilibili.com/cheese/play*
// @include      *search.bilibili.com*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @noframes
// ==/UserScript==

GM_addStyle(`
  html {
    overflow-y: scroll;
  }
  html.hideScroll {
    overflow: hidden;
    margin-left: -3px;
  }
  body::-webkit-scrollbar {
    width: 6px;
  }
  body::-webkit-scrollbar-corner,
  body::-webkit-scrollbar-track {
    background-color: #f8f8f8;
  }
  body::-webkit-scrollbar-thumb {
    background: #c5c5c5;
  }
  
  #bilibili-player {
    position: relative;
  }
  #spsy_msg {
    width: 105px;
    height: 42px;
    text-align: center;
    line-height: 42px;
    border-radius: 4px;
    background: rgba(255,255,255,.8);
    color: #222;
    font-size: 16px;
    position: absolute;
    top: -80px;
    right: 0;
    bottom: 0;
    left: 0;
    margin: auto;
    z-index: 999888;
    display: none;
  }
  #bl_info_xz {
    height: 25px;
    line-height: 25px;
    font-size: 14px;
    padding: 0 5px;
    color: #00a1d6;
    position: absolute;
    top: -25px;
    right: 0;
    z-index: 2;
  }
  .bilibili-player-video-btn-volume,
  .bilibili-player-video-btn-speed {
    opacity: 0.4;
    pointer-events: none;
  }
  .bilibili-player-volumeHint,
  .bpx-player-volume-hint {
    display: none !important;
  }
  .so-fg5r .inp {
    width: 50px;
    color: #666;
    border: 1px solid #0ad;
    border-radius: 2px;
    padding: 2px 5px;
    outline: none;
  }
  .so-fg5r .inp:focus {
    border-color: #0ad;
  }
  .so-fg5r span {
    color: #666;
  }
  .video-item.hide_7s {
    display: none !important;
  }
`)

const pks = ['video', 'bangumi', 'medialist']
if (
  pks.some(v => location.pathname.includes(v)) &&
  !location.host.includes('search')
) {
  document.documentElement.classList.add('hideScroll')
}

const G = {
  vdo: null,
  timer1s: 0,
  timer2s: 0,
  timer3s: 0,
  focusChangeTime: 0,
  g2URL: '',
}

function timeout(ms = 0) {
  return new Promise(resolve => setTimeout(resolve, ms))
}
function round(val, n = 2) {
  return Number(`${Math.round(`${val}e${n}`)}e-${n}`)
}
function floor(val, n = 2) {
  return Number(`${Math.floor(`${val}e${n}`)}e-${n}`)
}
function loadEl(fnz) {
  return new Promise(resolve => {
    const iFn = () => {
      let el = typeof fnz === 'function' ? fnz() : $(fnz)
      if (el.length) {
        return resolve(el)
      }
      setTimeout(iFn, 50)
    }
    iFn()
  })
}
function watchDom(el, ...args) {
  if (typeof el === 'string') {
    el = document.querySelector(el)
  }
  let opt = args.length > 1 ? args[0] : {}
  let cb = args.length > 1 ? args[1] : args[0]
  return new MutationObserver((mutations, observer) => {
    cb(mutations)
  }).observe(el, Object.assign({
    childList: true,
    attributes: true,
    characterData: true,
    subtree: true,
  }, opt))
}
async function getVideoWrap() {
  const sltor = location.pathname.includes('bangumi') ? 
  '.bpx-player-video-area' : 
  '#bilibiliPlayer'
  return (await loadEl(sltor))[0]
}
async function appendMsgLay() {
  let wp = await getVideoWrap()
  let msg = document.getElementById('spsy_msg')
  if (!wp.contains(msg)) {
    wp.insertAdjacentHTML('beforeend', `<div id="spsy_msg"></div>`)
  }
}
async function appendAxInfo() {
  let wp = await getVideoWrap()
  let inf = document.getElementById('bl_info_xz')
  if (!wp.contains(inf)) {
    wp.insertAdjacentHTML('beforeend', `<div id="bl_info_xz"></div>`)
  }
}
function setAxInfo() {
  let inf = $('#bl_info_xz')
  let vol = Math.trunc(getGMvolume() * 100)
  let speed = getGMspeed()
  if (inf.length) {
    speed = speed === 1 ? speed : `<span style="color:#f33;">${speed}</span>`
    inf.html(`<span>速度: ${speed} &nbsp; 音量: ${vol}</span>`)
  }
}
function toggleVideoFullscreen() {
  try {
    $('.bilibili-player-video-btn-fullscreen')[0].click()
  } catch (err) {}
  try {
    $('.squirtle-video-fullscreen > div')[0].click()
  } catch (err) {}
}
function setGMspeed(val) {
  return GM_setValue('--> bl_player_speed', val)
}
function setGMvolume(val) {
  GM_setValue('--> bl_player_volume', val)
}
function getGMspeed() {
  return +GM_getValue('--> bl_player_speed') || 1
}
function getGMvolume() {
  let vol = GM_getValue('--> bl_player_volume')
  return vol !== undefined ? vol : 0.5
}
// 判断是否全屏
function isFullScreen() {
  return document.isFullScreen || document.mozIsFullScreen || document.webkitIsFullScreen
}
// 显示信息
function showSpMsg(msg, type = '速度') {
  let mp = $('#spsy_msg')
  clearTimeout(G.timer2s)
  mp.fadeIn(180)
  mp.text(`${type} ${msg}`)
  G.timer2s = setTimeout(() => {
    mp.fadeOut(350)
  }, 800)
}
// 设置播放器播放速度
async function setPlayerSpeed(speedVal = getGMspeed()) {
  G.vdo.playbackRate = speedVal
  setGMspeed(speedVal)
  setAxInfo()
}
// 设置音量
async function setVolume(vol = getGMvolume()) {
  G.vdo.volume = vol
  setGMvolume(vol)
  setAxInfo()
}

function initVideoCfg() {
  clearTimeout(G.timer3s)
  G.timer3s = setTimeout(() => {
    appendMsgLay()
    appendAxInfo()
    setPlayerSpeed()
    setVolume()
  }, 10)
}

function debounce(func, delay) {
  let timer = null
  return function() {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

function zTimer() {
  initVideoCfg()
  setTimeout(zTimer, 2000)
}

async function regWpMouseEvt() {
  const wwp = $('#playerWrap')
  const wp = wwp.length ? wwp : await getVideoWrap()
  $(wp).on('mouseenter', () => {
    $('html').addClass('hideScroll')
  })
  $(wp).on('mouseleave', () => {
    $('html').removeClass('hideScroll')
  })
}

function watchUrlFunc() {
  if (G.g2URL !== location.href) {
    G.g2URL = location.href
    eachSearchResultAndAddHideCls()
  }
  setTimeout(watchUrlFunc, 500)
}

function judgeIsAppendSoMod() {
  const html = `
    <div class="so-fg5r">
      <input class="inp inp-s" type="text">
      <span> - </span>
      <input class="inp inp-e" type="text">
      <span>分钟</span>
    </div>
  `
  $('ul.filter-type.duration').append(html)
}

function eachSearchResultAndAddHideCls() {
  const min = $('.inp-s').val() || 0
  const max = $('.inp-e').val() || 1e9
  if (!min && !max) {
    $('.video-list > li.video-item').each((i, v) => {
      $(v).removeClass('hide_7s')
    })
    return
  }
  
  $('.video-list > li.video-item').each((i, v) => {
    let tis = $(v)
    tis.removeClass('hide_7s')
    let sTime = tis.find('.so-imgTag_rb').text()
    let mth = sTime.match(/\d{2}(?=(\:\d{2}){2})/)
    let hour = mth ? mth[0] : 0
    let minute = sTime.replace(/(?:\d{2}\:)?(\d{2})\:\d{2}/, '$1')
    let total = Number(hour) * 60 + Number(minute)
    
    if (total < Number(min) || total > Number(max)) {
      tis.closest('.video-item').addClass('hide_7s')
    }
  })
}
   
// 滚轮中键点击(滚轮点击)切换全屏
$('body').on('mousedown', '.bilibili-player-video-wrap,.bpx-player-video-area', function(e) {
  if (e.button === 1) {
    e.preventDefault()
    toggleVideoFullscreen()
  }
})

// 筛选搜索结果
$('body').on('input', '.inp-s, .inp-e', function() {
  eachSearchResultAndAddHideCls()
})

// ctrl和alt按下后短时间内阻止zxc
$('body').on('keydown', function(e) {
  if (/up|down/i.test(e.key)) {
    e.stopPropagation()
    e.preventDefault()
  }
})

// 键盘快捷键
$('body').on('keyup', async function(e) {
  if (e.target.nodeName !== 'BODY') return
  
  if (/^[zxc]$/.test(e.key)) {
    await timeout(100)
    if (Date.now() - G.focusChangeTime < 1000) return
    let val = getGMspeed()
    if (e.key === 'z') {
      val = 1
    }
    if (e.key === 'x') {
      val = Math.max(val - 0.25, 0.25)
    }
    if (e.key === 'c') {
      val = Math.min(val + 0.25, 3)
    }
    setPlayerSpeed(val)
    showSpMsg(val)
  }
  
  if (/up|down/i.test(e.key)) {
    let vl = getGMvolume()
    let vol = e.key.includes('Up') ? Math.min(vl + 0.03, 1) : Math.max(vl - 0.03, 0)
    setVolume(floor(vol))
    showSpMsg(Math.trunc(vol * 100), '音量')
  }
  
})
  
// 滚轮调节音量
window.addEventListener('wheel', async (e) => {
  const wp = await getVideoWrap()
  const isContains = wp.contains(e.target)
  
  if (!isContains) return
  
  let vol = getGMvolume()
  
  if (e.deltaY > 0) {
    // 减少音量
    vol = Math.max(vol - (e.altKey ? 0.1 : 0.03), 0)
  } else {
    // 增加音量
    vol = Math.min(vol + (e.altKey ? 0.1 : 0.03), 1)
  }
  vol = floor(vol)
  setVolume(vol)
  
  let pVol = Math.trunc(vol * 100)
  showSpMsg(pVol, '音量')
})

async function initFunc() {
  ['focus', 'blur'].forEach(v => {
    window.addEventListener(v, e => {
      G.focusChangeTime = Date.now()
    })
  })
  
  if (location.host.includes('search.bilibili.com')) {
    judgeIsAppendSoMod()
    watchUrlFunc()
  } else {
    G.vdo = (await loadEl('video'))[0]
    zTimer()
    G.vdo.addEventListener('play', () => {
      initVideoCfg()
      regWpMouseEvt()
    })
    regWpMouseEvt()
  }
}

initFunc()