您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
获取 B 站视频简介中 N 站视频的实时信息,包括播放量、弹幕数、简介等,并显示在视频简介中。
// ==UserScript== // @name N站视频信息查询 // @namespace http://tampermonkey.net/ // @version 0.6 // @description 获取 B 站视频简介中 N 站视频的实时信息,包括播放量、弹幕数、简介等,并显示在视频简介中。 // @author ctrn43062 // @match *://*.bilibili.com/video/* // @icon https://www.bilibili.com/favicon.ico // @grant none // @license MIT // @note v0.6 优化请求逻辑,减少请求次数;请更新该版本,否则脚本将无法使用 // @note v0.5 bug 修复 // @note v0.4 信息格式调整为播放等数据在视频标题下;支持自定义视频数据位置 // @note v0.3 替换跳转链接 acg.tv 为 N 视频链接 // @note v0.2 适配旧版播放页 // ==/UserScript== // 修复请求失败替换原简介的bug const REVERSE_PROXY_API = 'https://f7z1to.deta.dev' // 视频信息是否在原视频简介前插入 // true 是 // false 否 const INSERT_INFO_BEFORE = false function getURL() { return location.origin + location.pathname } function toLink(type, target, text) { const BASE_URL = 'https://www.nicovideo.jp' const PATHS = { 'video': 'watch', 'user': 'user', 'tag': 'tag' } let href = `${BASE_URL}/${PATHS[type]}/${target}` return `<a href="${href}" target="_blank">${text}</a>` } async function getVideoInfoData(video_list) { const headers = { 'x-url': getURL(), 'x-title': encodeURIComponent(document.title), 'content-type': 'application/json;' } try { console.log('[DEBUG]', 'requesting', video_list); return await fetch(REVERSE_PROXY_API, { method: 'POST', body: JSON.stringify(video_list), headers }).then(resp => resp.json()) } catch (e) { console.log('error', e) return { code: -1, status: '请求接口出错: ' + e.toString(), data: [] } } } function parseVideoInfo(sm, data) { const xml = (new DOMParser()).parseFromString(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://www.nicovideo.jp/watch/$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'); if (info['status'] !== 'ok') { infoEle.innerHTML = `<strong>出错了:${info['status']}</strong><br/>`; return infoEle; } infoEle.innerHTML = `<strong>${toLink('video', info['id'], info['id'])} 的详细信息:</strong> 标题: ${info['title']} 播放量: ${info['view']} 评论数(弹幕数): ${info['comment']} 收藏量: ${info['favorite']} 简介: ${info['description'] || '(无简介)'} 投稿时间: ${(new Date(info['post_at'])).toLocaleString()} 投稿者: ${toLink('user', info['user_id'], info['username'])} ${info['tags']} ` return infoEle; } function insertVideoInfoToDesc(data) { const element = createVideoInfoElement(data).innerHTML; const container = document.querySelector('.desc-info.desc-v2 > span'); const html = container.innerHTML const title = `<strong>${INSERT_INFO_BEFORE ? '原始简介:' : ''}</strong><br/>` if (INSERT_INFO_BEFORE) { container.innerHTML = element + '\n' + title + html } else { container.innerHTML = html + '<br/><br/>' + element } container.innerHTML = `<span class='nico-video'>${container.innerHTML}</span>` } async function setDescription(description) { if (!setDescription.cache) { setDescription.cache = {} } const cache = setDescription.cache const id_list = new Set(description); if(!id_list.size) { return ; } // 如果简介长度无需折叠,则不会显示展开按钮。但是加上视频详情后可能需要折叠,所以强制开启折叠按钮 const toggleBtn = document.querySelector('.toggle-btn'); if (toggleBtn) { toggleBtn.style.display = 'block'; } const resp = await getVideoInfoData(id_list) resp.data.forEach(info => { const key = info.key const data = cache[key] || parseVideoInfo(key, info.data) insertVideoInfoToDesc(data) cache[key] = data }) if (resp.code === -1) { insertVideoInfoToDesc(createVideoInfoElement(resp)) } } function watingForPageLoaded() { return new Promise((resolve) => { const descEle = document.querySelector('.desc-info.desc-v2') const isOldStyle = document.querySelector('.tip-info') const it = setInterval(() => { if ((descEle.style.height || isOldStyle) && !descEle.querySelector('.nico-video')) { clearInterval(it) resolve() } }, 10); }) } (function () { let currentURL = getURL() new MutationObserver(async () => { const url = getURL() if (url !== currentURL) { currentURL = url; onUrlChange(); } }).observe(document.head, { subtree: true, childList: true }); async function onUrlChange() { await watingForPageLoaded() const descriptionEle = document.querySelector('.desc-info.desc-v2 > span') const description = descriptionEle.textContent.match(/sm\d+/g) setDescription(description) } onUrlChange() })();