您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
获取 B 站视频简介中 N 站视频的实时信息,包括播放量、弹幕数、简介等,并显示在视频简介中。
当前为
// ==UserScript== // @name N站视频信息查询 // @namespace http://tampermonkey.net/ // @version 0.2 // @description 获取 B 站视频简介中 N 站视频的实时信息,包括播放量、弹幕数、简介等,并显示在视频简介中。 // @author ctrn43062 // @match *://*.bilibili.com/video/* // @icon https://www.bilibili.com/favicon.ico // @grant none // @license MIT // @note v0.2 适配旧版播放页 // ==/UserScript== const REVERSE_PROXY_API = 'https://api.nicovideo.workers.dev/' function toLink(type, target, text) { let href = '' switch (type) { case 'video': href = `https://acg.tv/${target}`; break; case 'user': href = `https://www.nicovideo.jp/user/${target}`; break; case 'tag': href = `https://www.nicovideo.jp/tag/${target}`; break; } return `<a href="${href}" target="_blank">${text}</a>` } async function getVideoInfoData(sm) { return await fetch(`${REVERSE_PROXY_API}/${sm}`).then(resp => resp.json()) } function parseVideoInfo(sm, data) { if (data['code'] !== 200) { // throw new Error(`Request API Response Error:${sm}\n${data}`) return { status: '请求 cf 接口失败。' } } const xml = (new DOMParser()).parseFromString(data['data'], 'text/xml'); const response = xml.firstChild; if (response.getAttribute('status') !== 'ok') { // throw new Error(`Request Video Info Error:${sm}\n${response}`) return { status: `获取 ${toLink('video', sm, sm)} 数据失败,视频可能已被删除。` } } function _parse() { const user_id = response.querySelector('user_id').textContent; const username = response.querySelector('user_nickname').textContent; const title = response.querySelector('title').textContent; const description = response.querySelector('description').textContent.replaceAll(/(sm\d+)/g, '<a href="https://acg.tv/$1" target="_blank">$1</a>'); const post_at = response.querySelector('first_retrieve').textContent; let view = +response.querySelector('view_counter').textContent; let comment = +response.querySelector('comment_num').textContent; let favorite = +response.querySelector('mylist_counter').textContent; const tagsEle = response.querySelectorAll('tags > tag'); const tags = []; tagsEle.forEach(tagEle => { tags.push(tagEle.textContent); }); const tags_link = tags.map(tag => toLink('tag', tag, tag)).join(' | ') const base = 10000; if (view >= base) { view = (view / base).toFixed(1) + '万'; } if (comment >= base) { comment = (comment / base).toFixed(1) + '万'; } if (favorite >= base) { favorite = (favorite / base).toFixed(1) + '万'; } return { status: 'ok', title, description, post_at, view, comment, favorite, tags: tags_link, user_id, username, id: sm } } return _parse(); } function createVideoInfoElement(info) { const infoEle = document.createElement('span'); infoEle.className = 'desc-info-text nico-video-detail'; if (info['status'] !== 'ok') { infoEle.innerHTML = `\n\n<strong>出错了:${info['status']}</strong>`; return infoEle; } infoEle.innerHTML = ` \n<strong>${toLink('video', info['id'], info['id'])} 的详细信息:</strong> 标题: ${info['title']} 简介: ${info['description'] || '(无简介)'} 投稿时间: ${(new Date(info['post_at'])).toLocaleString()} 播放量: ${info['view']} 评论数(弹幕数): ${info['comment']} 收藏量: ${info['favorite']} 投稿者: ${toLink('user', info['user_id'], info['username'])} ${info['tags']} ` return infoEle; } function insertVideoInfoToDesc(data) { const element = createVideoInfoElement(data); const container = document.querySelector('.desc-info.desc-v2'); container.appendChild(element); } (async function () { const observer = new MutationObserver((muls) => { for (const mul of muls) { // v0.1 B站视频简介不知道为什么会被重新加载一次,具体行为是删除原先的简介 DOM 然后插入新 DOM,因此这里通过判断是否有 DOM 被删除来决定是否插入数据 if (mul.removedNodes.length) { let oldDetailRemoved = false; for (const removedNode of mul.removedNodes) { // 移除旧元素时同样会触发 observer,需要判断当前移除的是否为旧元素,如果是则不发送请求 if (removedNode.nodeType === 1 && removedNode.classList.contains('nico-video-detail')) { oldDetailRemoved = true; break; } } document.querySelectorAll('.nico-video-detail').forEach(item => { item.remove(); }) if (oldDetailRemoved) { continue; } const video_ids = new Set(document.querySelector('.desc-info.desc-v2 > span').textContent.match(/sm\d+/g)); // 如果简介长度无需折叠,则不会显示展开按钮。但是加上视频详情后可能需要折叠,所以强制开启折叠按钮 const toggleBtn = document.querySelector('.toggle-btn'); if (toggleBtn) { toggleBtn.style.display = 'block'; } video_ids.forEach(async id => { console.log('[DEBUG]', 'requesting', id); // TODO: // 在这里插入 cache 相关代码 const data = parseVideoInfo(id, await getVideoInfoData(id)); insertVideoInfoToDesc(data); }); } } }) const container = document.querySelector('.desc-info.desc-v2'); observer.observe(container, { subtree: true, childList: true }); })();