b 站 - 显示视频 av 号、bv 号及弹幕 cid

在 b 站播放页标题栏下面显示视频 av 号、bv 号及弹幕 cid,支持复制短链接

当前为 2020-06-14 提交的版本,查看 最新版本

// ==UserScript==
// @name                bilibili - display video av, bv number below the title bar
// @name:zh-CN          b 站 - 显示视频 av 号、bv 号及弹幕 cid
// @namespace           https://gist.github.com/phtwo
// @version             0.3
// @licence             MIT. Copyright (c) phtwo.
// @description         Display the video av, bv, cid number below the title bar of the bilibili play page, support copy short link
// @description:zh-CN   在 b 站播放页标题栏下面显示视频 av 号、bv 号及弹幕 cid,支持复制短链接
// @match               *://www.bilibili.com/video/*
// @grant               GM_addStyle
//
// @author              phtwo
// @homepage            https://gist.github.com/phtwo/e2d8c04707ed6a6369a55be37e3f14c7
// @supportURL          https://gist.github.com/phtwo/e2d8c04707ed6a6369a55be37e3f14c7
//
// @noframes
// @nocompat Chrome
//
// ==/UserScript==

/**
 * @licence MIT. Copyright (c) Feross Aboukhadijeh.
 * @description From https://github.com/feross/clipboard-copy
 * @param text
 * @returns {Promise<void>|any}
 */
function clipboardCopy(text) {
  // Use the Async Clipboard API when available. Requires a secure browsing
  // context (i.e. HTTPS)
  if (navigator.clipboard) {
    return navigator.clipboard.writeText(text).catch(function (err) {
      throw (err !== undefined ? err : new DOMException('The request is not allowed', 'NotAllowedError'))
    })
  }

  // ...Otherwise, use document.execCommand() fallback

  // Put the text to copy into a <span>
  const span = document.createElement('span')
  span.textContent = text

  // Preserve consecutive spaces and newlines
  span.style.whiteSpace = 'pre'

  // Add the <span> to the page
  document.body.appendChild(span)

  // Make a selection object representing the range of text selected by the user
  const selection = window.getSelection()
  const range = window.document.createRange()
  selection.removeAllRanges()
  range.selectNode(span)
  selection.addRange(range)

  // Copy text to the clipboard
  let success = false
  try {
    success = window.document.execCommand('copy')
  } catch (err) {
    console.log('error', err)
  }

  // Cleanup
  selection.removeAllRanges()
  window.document.body.removeChild(span)

  return success
    ? Promise.resolve()
    : Promise.reject(new DOMException('The request is not allowed', 'NotAllowedError'))
}

(function () {

  const classPref = 'p-bevd'

  const styleCssText = `
.${classPref}-data-item {
  position: relative!important;
  color: #afafaf!important;
}

.${classPref}-data-item:hover .${classPref}-hover-box {
  display: inline-block!important;
}

.${classPref}-hover-box {
  display: none!important;
  vertical-align: top!important;
  
  position: absolute!important;
  bottom: 1.2em;
  right: 0;
  padding: 0 0 0 10px!important;
}

.${classPref}-copy-link {
  color: #afafaf!important;
}

.${classPref}-copy-link:hover {
  color: #00a1d6!important;
}
  `

  const urlNodeText = '短链接'

  const list = []

  /** @type Element */
  let videoDataLineElem

  init()

  function init() {
    console.log('bilibili_extra_video_data init')

    initVideoDataLineElem()
    if (!videoDataLineElem) {
      return
    }

    util_addStyle(styleCssText)

    initMonitor()
  }

  function initVideoDataLineElem() {
    videoDataLineElem = document.querySelector('#viewbox_report .video-data:nth-of-type(2)')
  }

  function initMonitor() {
    const observer = new MutationObserver(mutations => {
      for (const mutation of mutations) {
        if ('attributes' === mutation.type) {
          observer.takeRecords()
          run()
          break
        }
      }
    })

    observer.observe(videoDataLineElem.children[0], {
      attributes: true,
      attributeFilter: ['title'],
    })
  }

  function run() {
    const {aid, bvid, cid} = unsafeWindow

    list.length = 0 // clear list
    addItem('av', aid, true)
    addItem('', bvid, true)
    addItem('cid', cid)

    const fragment = createFragment()

    const referenceNode = videoDataLineElem.querySelector('.copyright')
    removeOldNodes()
    videoDataLineElem.insertBefore(fragment, referenceNode)

    console.log('bilibili_extra_video_data run finished')
  }

  function createFragment() {
    const firstPrefix = '&nbsp;&nbsp;'
    const prefix = '&nbsp;·&nbsp;'

    const nodeList = list.map((item, index) => {
      let text = item.prefix + item.value
      let innerHTML = (index === 0 ? firstPrefix : prefix) + text

      const elem = document.createElement('span')
      elem.title = text
      elem.innerHTML = innerHTML
      elem.classList.add(`${classPref}-data-item`)
      elem.setAttribute('data-inject', '')

      item.hasShortLink && elem.append(createHoverElem(text))

      return elem
    })

    const fragment = document.createDocumentFragment()
    fragment.append(...nodeList)
    return fragment
  }

  function createHoverElem(id) {

    const aNode = document.createElement('a')
    aNode.classList.add(`${classPref}-copy-link`)
    aNode.href = 'https://b23.tv/' + id
    aNode.textContent = urlNodeText
    aNode.title = '单击复制,右键打开菜单'

    aNode.addEventListener('click', copyUrlOnClick)

    const e = document.createElement('span')
    e.classList.add(`${classPref}-hover-box`)
    e.append(aNode)
    return e
  }

  function copyUrlOnClick(ev) {
    ev.preventDefault()
    ev.stopPropagation()

    const target = ev.target
    const text = target.href || ''

    clipboardCopy(text)
      .then(async () => {
        console.log('bilibili_extra_video_data copy [%s] to clipboard succeeded.', text)
        target.textContent = '已复制!'
        await util_delay(1e3)
        target.textContent = urlNodeText
      }, () => {
        console.error('bilibili_extra_video_data copy [%s] to clipboard failed.', text)
        window.prompt('自动复制失败,请手动复制', text)
      })
  }

  function removeOldNodes() {
    videoDataLineElem
      .querySelectorAll('[data-inject]')
      .forEach(node => node.parentNode.removeChild(node))
  }

  function addItem(prefix = '', value, hasShortLink = false) {
    value && list.push({prefix, value, hasShortLink})
  }

  function util_addStyle(css) {
    if (typeof GM_addStyle !== 'undefined') {
      return GM_addStyle(css)
    }

    const styleNode = document.createElement("style")
    styleNode.appendChild(document.createTextNode(css))
    ;(document.querySelector("head") || document.documentElement).appendChild(styleNode)
    return styleNode
  }

  function util_delay(timeout = 300, doReject) {
    return new Promise((resolve, reject) => {
      setTimeout(doReject ? reject : resolve, timeout)
    })
  }

})()