Greasy Fork

Greasy Fork is available in English.

B站封面获取

B站视频播放页(普通模式、稍后再看模式)、番剧播放页、直播间添加获取封面的按钮

当前为 2020-07-18 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @id             BilibiliCover@Laster2800
// @name           B站封面获取
// @version        4.1.1.20200718
// @namespace      laster2800
// @author         Laster2800
// @description    B站视频播放页(普通模式、稍后再看模式)、番剧播放页、直播间添加获取封面的按钮
// @include        *://www.bilibili.com/video/*
// @include        *://www.bilibili.com/bangumi/play/*
// @include        *://live.bilibili.com/*
// @include        *://www.bilibili.com/medialist/play/watchlater/*
// @exclude        *://live.bilibili.com/
// @exclude        *://live.bilibili.com/p/html/live-web-mng/*
// @grant          GM_xmlhttpRequest
// @connect        api.bilibili.com
// @grant          GM_addStyle
// ==/UserScript==

(function() {
  if (/\/video\//.test(location.href)) {
    executeAfterConditionPass({
      condition: () => {
        var app = document.querySelector('#app')
        var vueLoad = app && app.__vue__
        if (!vueLoad) {
          return false
        }
        return document.querySelector('#arc_toolbar_report')
      },
      callback: addVideoBtn,
    })
  } else if (/\/bangumi\/play\//.test(location.href)) {
    executeAfterConditionPass({
      condition: () => {
        var app = document.querySelector('#app')
        var vueLoad = app && app.__vue__
        if (!vueLoad) {
          return false
        }
        return document.querySelector('#toolbar_module')
      },
      callback: addBangumiBtn,
    })
  } else if (/live\.bilibili\.com\/\d/.test(location.href)) {
    executeAfterConditionPass({
      condition: () => {
        var hiVm = document.querySelector('#head-info-vm')
        var vueLoad = hiVm && hiVm.__vue__
        if (!vueLoad) {
          return false
        }
        return hiVm.querySelector('.room-info-upper-row .upper-right-ctnr')
      },
      callback: addLiveBtn,
    })
  } else if (/\/medialist\/play\/watchlater(?=\/|$)/.test(location.href)) {
    executeAfterConditionPass({
      condition: () => {
        var app = document.querySelector('#app')
        var vueLoad = app && app.__vue__
        if (!vueLoad) {
          return false
        }
        return app.querySelector('#playContainer .left-container .play-options .play-options-more')
      },
      callback: addWatchlaterVideoBtn,
    })
  }
})()

function addVideoBtn(atr) {
  var coverMeta = document.querySelector('head meta[itemprop=image]')
  var coverUrl = coverMeta && coverMeta.content
  var cover = document.createElement('a')
  var errorMsg = '获取失败,若非网络问题请提供反馈'
  cover.innerText = '获取封面'
  cover.target = '_blank'
  if (coverUrl) {
    cover.href = coverUrl
  } else {
    cover.onclick = () => alert(errorMsg)
  }
  cover.title = coverUrl || errorMsg
  cover.className = 'appeal-text'
  atr.appendChild(cover)
}

function addBangumiBtn(tm) {
  GM_addStyle(`
.cover_btn {
    float: right;
    cursor: pointer;
    font-size: 12px;
    margin-right: 16px;
    line-height: 36px;
    color: #505050;
}
.cover_btn:hover {
    color: #00a1d6;
}
    `)
  var coverMeta = document.querySelector('head meta[property="og:image"]')
  var coverUrl = coverMeta && coverMeta.content
  var cover = document.createElement('a')
  var errorMsg = '获取失败,若非网络问题请提供反馈'
  cover.innerText = '获取封面'
  cover.target = '_blank'
  if (coverUrl) {
    cover.href = coverUrl
  } else {
    cover.onclick = () => alert(errorMsg)
  }
  cover.title = coverUrl || errorMsg
  cover.className = 'cover_btn'
  tm.appendChild(cover)
}

function addLiveBtn(urc) {
  GM_addStyle(`
.cover_btn {
    cursor: pointer;
    color: rgb(153, 153, 153);
}
.cover_btn:hover {
    color: #23ade5;
}
  `)

  try {
    // 这个 __NEPTUNE_IS_MY_WAIFU__ 在最外层的 window 对象上,因为变量名覆盖的原因无法获取到该 window,只能直接用
    // eslint-disable-next-line no-undef
    var data = __NEPTUNE_IS_MY_WAIFU__.baseInfoRes.data
    var coverUrl = data.user_cover
    var kfUrl = data.keyframe
  } catch (e) {
    console.error(e)
  }
  var cover = document.createElement('a')
  cover.innerText = '获取封面'
  cover.target = '_blank'
  if (coverUrl) {
    cover.href = coverUrl
    cover.title = coverUrl
  } else if (kfUrl) {
    cover.href = kfUrl
    cover.title = '直播间没有设置封面,或者因不明原因无法获取到封面,点击获取关键帧:\n' + kfUrl
  } else {
    var errorMsg = '获取失败,若非网络问题请提供反馈'
    cover.onclick = () => alert(errorMsg)
    cover.title = errorMsg
  }
  cover.className = 'cover_btn'
  urc.insertBefore(cover, urc.firstChild)
}

function addWatchlaterVideoBtn(pom) {
  GM_addStyle(`
.cover_btn {
    cursor: pointer;
    float: left;
    margin-right: 1em;
    font-size: 12px;
    color: #757575;
}
.cover_btn:hover {
    color: #23ade5;
}
  `)

  var cover = document.createElement('a')
  var errorMsg = '获取失败,可能是因为该视频已经移除出稍后再看;若非该原因或网络问题,请提供反馈'
  cover.innerText = '获取封面'
  cover.target = '_blank'
  cover.className = 'cover_btn'
  cover.onclick = e => e.stopPropagation()
  pom.appendChild(cover)

  var updateCoverUrl = () => {
    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://api.bilibili.com/x/v2/history/toview/web?jsonp=jsonp',
      onload: function(response) {
        if (response && response.responseText) {
          try {
            executeAfterConditionPass({
              condition: () => {
                try {
                  var url = document.querySelector('.play-title-location').href
                  var m = url.match(/(?<=\/)BV[a-zA-Z\d]+(?=\/|$)/)
                  if (m && m[0]) {
                    return m[0]
                  }
                } catch (e) {
                  // ignore
                }
              },
              callback: bvid => {
                var json = JSON.parse(response.responseText)
                var watchlaterList = json.data.list

                var coverUrl = null
                for (var e of watchlaterList) {
                  if (bvid == e.bvid) {
                    coverUrl = e.pic
                    break
                  }
                }

                if (coverUrl) {
                  cover.href = coverUrl
                } else {
                  cover.onclick = () => alert(errorMsg)
                }
                cover.title = coverUrl || errorMsg
              }
            })
          } catch (e) {
            console.error(e)
          }
        }
      }
    })
  }

  updateCoverUrl()

  // 创建 locationchange 事件
  // https://stackoverflow.com/a/52809105
  history.pushState = (f => function pushState() {
    var ret = f.apply(this, arguments)
    window.dispatchEvent(new Event('pushstate'))
    window.dispatchEvent(new Event('locationchange'))
    return ret
  })(history.pushState)
  history.replaceState = (f => function replaceState() {
    var ret = f.apply(this, arguments)
    window.dispatchEvent(new Event('replacestate'))
    window.dispatchEvent(new Event('locationchange'))
    return ret
  })(history.replaceState)
  window.addEventListener('popstate', () => {
    window.dispatchEvent(new Event('locationchange'))
  })

  window.addEventListener('locationchange', function() {
    updateCoverUrl()
  })
}

/**
 * 在条件满足后执行操作
 *
 * 当条件满足后,如果不存在终止条件,那么直接执行 callback(result)。
 *
 * 当条件满足后,如果存在终止条件,且 stopTimeout 大于 0,则还会在接下来的 stopTimeout 时间内判断是否满足终止条件,称为终止条件的二次判断。
 * 如果在此期间,终止条件通过,则表示依然不满足条件,故执行 stopCallback() 而非 callback(result)。
 * 如果在此期间,终止条件一直失败,则顺利通过检测,执行 callback(result)。
 *
 * @param {Object} options 选项
 * @param {Function} options.condition 条件,当 condition() 返回的 result 为真值时满足条件
 * @param {Function} options.callback 当满足条件时执行 callback(result)
 * @param {number} [options.interval=100] 检测时间间隔(单位:ms)
 * @param {number} [options.timeout=5000] 检测超时时间,检测时间超过该值时终止检测(单位:ms)
 * @param {Function} options.onTimeout 检测超时时执行 onTimeout()
 * @param {Function} options.stopCondition 终止条件,当 stopCondition() 返回的 stopResult 为真值时终止检测
 * @param {Function} options.stopCallback 终止条件达成时执行 stopCallback()(包括终止条件的二次判断达成)
 * @param {number} [options.stopInterval=50] 终止条件二次判断期间的检测时间间隔(单位:ms)
 * @param {number} [options.stopTimeout=0] 终止条件二次判断期间的检测超时时间(单位:ms)
 */
function executeAfterConditionPass(options) {
  var defaultOptions = {
    condition: () => true,
    callback: result => console.log(result),
    interval: 100,
    timeout: 5000,
    onTimeout: null,
    stopCondition: null,
    stopCallback: null,
    stopInterval: 50,
    stopTimeout: 0,
  }
  var o = {
    ...defaultOptions,
    ...options
  }
  if (!(o.callback instanceof Function)) {
    return
  }

  var cnt = 0
  var maxCnt = o.timeout / o.interval
  var tid = setInterval(() => {
    var result = o.condition()
    var stopResult = o.stopCondition && o.stopCondition()
    if (stopResult) {
      clearInterval(tid)
      o.stopCallback instanceof Function && o.stopCallback()
    } else if (++cnt > maxCnt) {
      clearInterval(tid)
      o.onTimeout instanceof Function && o.onTimeout()
    } else if (result) {
      clearInterval(tid)
      if (o.stopCondition && o.stopTimeout > 0) {
        executeAfterConditionPass({
          condition: o.stopCondition,
          callback: o.stopCallback,
          interval: o.stopInterval,
          timeout: o.stopTimeout,
          onTimeout: () => o.callback(result)
        })
      } else {
        o.callback(result)
      }
    }
  }, o.interval)
}