您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 b 站播放页标题栏下面显示视频 av 号、bv 号及弹幕 cid,支持复制短链接
// ==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.2 // @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) start().catch((err) => { console.error( '[bilibili_extra_video_data] 发生未知错误,若刷新仍报错,可能为页面更新导致,', err ) }) } async function start() { await waitingForTheFirstInjection() initVideoDataChangeMonitor() doInject() } async function waitingForTheFirstInjection() { let lastElem lastElem = await waitingForChildElement( document.querySelector('#bilibili-player'), '.bilibili-player-video-sendbar', true ) lastElem = await waitingForChildElement( lastElem, '.bilibili-player-video-info-people-number', true ) console.log( '[bilibili_extra_video_data] Element(video-info-people-number) display' ) // observeElementChange( // lastElem, // ({ off }) => { // console.log( // '[bilibili_extra_video_data] Element(video-info-people-number)' // ) // }, // { // attributes: false, // characterData: true, // characterDataOldValue: true, // } // ) // Trick Code: 检测到上述元素后,设置 2s 的延时 await util_delay(2e3) } function initVideoDataLineElem() { videoDataLineElem = document.querySelector( '#viewbox_report .video-data:nth-of-type(1)' ) } function initVideoDataChangeMonitor() { observeElementChange(videoDataLineElem.children[0], doInject, { attributeFilter: ['title'], }) } function doInject() { 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] doInject finished') } function createFragment() { const firstPrefix = ' ' const prefix = ' · ' 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 }) } // ------ /** * @name waitingForChildElement * @description 使用 MutationObserver 接口,等待父元素后代中匹配指定 CSS 选择器元素的出现并返回该元素 * @param {Node | string} parent * @param {string} childSelectors - 使用 Element.matches 匹配 * @param {boolean} subtree * @return {Promise<Element>} */ function waitingForChildElement(parent, childSelectors, subtree = false) { // 先判断 后代元素 是否存在,存在立即返回 // 不存在,再使用 MutationObserver 监视 const deferred = createPromiseDeferred() const parentElem = typeof parent === 'string' ? document.querySelector(parent) : parent const targetChildElem = subtree ? parentElem.querySelector(childSelectors) : [...parentElem.children].find((elem) => elem.matches(childSelectors)) if (targetChildElem) { deferred.resolve(targetChildElem) return deferred.promise } const options = { childList: true, subtree, } const observer = new MutationObserver(mutationCallback) observer.observe(parentElem, options) return deferred.promise function mutationCallback(/** @type {Array<MutationRecord>} */ recordList) { for (const record of recordList) { let targetNode for (const node of [...record.addedNodes]) { if (1 !== node.nodeType) { continue } if (node.matches(childSelectors)) { targetNode = node break } if (subtree) { let tNode = node.querySelector(childSelectors) if (tNode) { targetNode = tNode break } } } if (targetNode) { observer.disconnect() deferred.resolve(targetNode) break } } } } /** * observeElementChange * @description 监听元素属性的变化。 ps: 批量更新多个属性时仅触发一次 * @param {Element} target * @param {Function} callback * @param {Object} options */ function observeElementChange(target, callback, options) { const observer = new MutationObserver((recordList) => { observer.takeRecords() callback({ off, }) }) observer.observe(target, { attributes: true, // attributeFilter: [], // subtree: true, // childList: true, // characterData: true, ...options, }) function off() { observer.disconnect() } } // ------ function createPromiseDeferred() { let _resolve, _reject let promise = new Promise((resolve, reject) => { _resolve = resolve _reject = reject }) return { promise, resolve: _resolve, reject: _reject, } } 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) }) } })()