返回首頁 

BV2AV + 视频统计

支持 Safari | 干净 URL | 视频统计 | V 家成就

// ==UserScript==// @name         BV2AV + 视频统计// @description  支持 Safari | 干净 URL | 视频统计 | V 家成就// @version      3.1.6// @license      MIT// @author       Joseph Chris <[email protected]>// @icon         https://www.bilibili.com/favicon.ico// @namespace    https://github.com/baobao1270/util-scripts/blob/main/tampermonkey/bilibili-vcutils// @homepageURL  https://github.com/baobao1270/util-scripts/blob/main/tampermonkey/bilibili-vcutils// @supportURL   mailto:[email protected]// @compatible   firefox// @compatible   safari// @compatible   chrome// @compatible   edge// @match        *://www.bilibili.com/video/*// @match        *://www.bilibili.com/list/watchlater*// @match        *://www.bilibili.com/list/ml*// @grant        GM_registerMenuCommand// @grant        GM_unregisterMenuCommand// @grant        GM_setValue// @grant        GM_getValue// ==/UserScript==const VCUtil = {ConfigFlags: {video:      { stdurl: true, stdurlAll: true, stat: true },watchlater: { stdurl: true, stdurlAll: true, stat: true },medialist:  { stdurl: true, stdurlAll: true, stat: true },timezoneSlotsCount: 3,timezoneSlotDefault: ['PVG', 'NRT', 'LAX'],},};VCUtil.Convert = {bv2av: function (bvid) {const XOR_CODE = 23442827791579n;const MASK_CODE = 2251799813685247n;const BASE = 58n;const data = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';const bvidArr = Array.from(bvid);[bvidArr[3], bvidArr[9]] = [bvidArr[9], bvidArr[3]];[bvidArr[4], bvidArr[7]] = [bvidArr[7], bvidArr[4]];bvidArr.splice(0, 3);const tmp = bvidArr.reduce((pre, bvidChar) => pre * BASE + BigInt(data.indexOf(bvidChar)), 0n);return Number((tmp & MASK_CODE) ^ XOR_CODE);},av2bv: function (aid) {const XOR_CODE = 23442827791579n;const MAX_AID = 1n << 51n;const BASE = 58n;const data = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';const bytes = ['B', 'V', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0'];let bvIndex = bytes.length - 1;let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE;while (tmp > 0) {bytes[bvIndex] = data[Number(tmp % BigInt(BASE))];tmp = tmp / BASE;bvIndex -= 1;}[bytes[3], bytes[9]] = [bytes[9], bytes[3]];[bytes[4], bytes[7]] = [bytes[7], bytes[4]];return bytes.join('');},};VCUtil.URL = {Info: function () {if (location.pathname.startsWith("/video/")) {const videoId = window.location.pathname.slice('/video/'.length).replace(/\/$/, '');const avid = videoId.startsWith('av') ? Number(videoId.slice(2)) : VCUtil.Convert.bv2av(videoId);const part = new URL(window.location.href).searchParams.get("p") || 1;return {avid: avid, bvid: VCUtil.Convert.av2bv(avid), part: part, type: 'video',standardUrl: `https://www.bilibili.com/video/av${avid}${part > 1 ? `?p=${part}` : ''}`};}if (location.pathname.startsWith("/list/watchlater")) {const bvid = new URL(window.location.href).searchParams.get("bvid");const part = new URL(window.location.href).searchParams.get("p") || 1;const oid = new URL(window.location.href).searchParams.get("oid");return {avid: VCUtil.Convert.bv2av(bvid), bvid: bvid, part: part, type: 'watchlater',standardUrl: `https://www.bilibili.com/list/watchlater?oid=${oid}&bvid=${bvid}${part > 1 ? `&p=${part}` : ''}`};}if (location.pathname.startsWith("/list/ml")) {const mlid = location.pathname.slice('/list/ml'.length);const bvid = new URL(window.location.href).searchParams.get("bvid");const part = new URL(window.location.href).searchParams.get("p") || 1;const oid = new URL(window.location.href).searchParams.get("oid");return {avid: VCUtil.Convert.bv2av(bvid), bvid: bvid, part: part, type: 'medialist',standardUrl: `https://www.bilibili.com/list/ml${mlid}?oid=${oid}&bvid=${bvid}${part > 1 ? `&p=${part}` : ''}`};}return null;},Standardize: function (urlInfo) {if (!urlInfo.standardUrl) return;if (urlInfo.standardUrl !== window.location.href) {console.log("[B站视频统计] [URL] 当前 URL:", window.location.href);console.log("[B站视频统计] [URL] 重定向到标准URL:", urlInfo.standardUrl);history.replaceState(null, '', urlInfo.standardUrl);}},StandardizeAllUrl: function () {const links = Array.from(document.querySelectorAll('a[href]'));links.forEach(link => {const pathname = new URL(link.href).pathname;if (!pathname.startsWith('/video/')) return;const videoId = pathname.slice('/video/'.length).replace(/\/$/, '');const avid = videoId.startsWith('av') ? Number(videoId.slice(2)) : VCUtil.Convert.bv2av(videoId);const part = new URL(link.href).searchParams.get("p") || 1;link.href = `https://www.bilibili.com/video/av${avid}${part > 1 ? `?p=${part}` : ''}`;});},};VCUtil.Stat = {InfoBoxClass: '.vcutil-stat',FormatTimezoneSlots: function (block, timestamp) {for (let slotId = 0; slotId < VCUtil.ConfigFlags.timezoneSlotsCount; slotId++) {const { current } = VCUtil.MenuCommand.CurrentAndNextTimezone(slotId);const { IATA, timezone } = current;console.log(`[B站视频统计] [Stat] 时区槽位 ${slotId + 1} = IATA: ${IATA}, 时区: ${timezone}`);if (timezone === "Timestamp") {block.AddText(`${IATA} ${timestamp}`, true);} else {block.AddText(`${IATA} ${VCUtil.Stat.Format.Timezone(timestamp, timezone)}`, true);}}},FetchData: async function (avid, part) {if (!document.querySelector('div.bili-avatar')) return console.log("[B站视频统计] [Stat] Header 未完全加载,推迟 FetchData");if (document.querySelector(VCUtil.Stat.InfoBoxClass)&& document.querySelector(VCUtil.Stat.InfoBoxClass).getAttribute('data-avid') === avid.toString()) return;const url = `https://api.bilibili.com/medialist/gateway/base/resource/info?rid=${avid}&type=2`;console.log("[B站视频统计] [Stat] FetchData:", `HTTP GET ${url}`);const response = await fetch(url);console.log("[B站视频统计] [Stat] FetchData: Response", response);const json = await response.json();console.log("[B站视频统计] [Stat] FetchData: JSON", json);if (json == undefined) return;const data = json.data;if (data == undefined) return;const format = VCUtil.Stat.Format;const block = VCUtil.Stat.BuildInfoBox(avid).AddText(`${format.HumanReadableNumber(data.cnt_info.play)} 播放    `).AddText(format.Tname(data.tid)).AddText(`av${avid}`, true).AddText(VCUtil.Convert.av2bv(avid), true).AddText(`cid=${data.pages[part - 1].id}`, true).AddText((data.tid === 30) ? format.VocaloidAchievement(data.cnt_info.play) : "非 VU 区视频").AddLineBreak();block.AddText('发布');VCUtil.Stat.FormatTimezoneSlots(block, data.pubtime);block.AddLineBreak();block.AddText('投稿');VCUtil.Stat.FormatTimezoneSlots(block, data.ctime);block.AddLineBreak();if (data.tid === 30) {block.AddLink('TDD',`https://tdd.bunnyxt.com/video/av${avid}`)} else {block.AddText('---');}block.AddLink('封面', data.cover);block.AddLink('短链接', `https://b23.tv/av${avid}`);block.AddLink('API', url);VCUtil.Stat.UpdateLayout();},UpdateLayout: function () {const infoBox = document.querySelector(VCUtil.Stat.InfoBoxClass);if (infoBox) {document.querySelector(".video-info-meta").parentElement.style.paddingBottom = `${infoBox.clientHeight + 80}px`;}},BuildInfoBox: function (avid) {const infoBox = document.createElement('div');infoBox.className = VCUtil.Stat.InfoBoxClass.slice(1);infoBox.setAttribute('data-avid', avid.toString());infoBox.style.color = "#999";infoBox.style.fontSize = "12px";infoBox.style.lineHeight = "1.5";infoBox.style.position = "absolute";infoBox.style.zIndex = "10";infoBox.style.paddingTop = "10px";Array.from(document.querySelectorAll(VCUtil.Stat.InfoBoxClass)).forEach(e => e.remove());document.querySelector(".video-info-meta").parentElement.appendChild(infoBox);const builder = {AddText: function (text, code = false) {const textBox = document.createElement("span");textBox.innerText = text + " ";textBox.style.marginRight = "13px";if (code === true) { textBox.style.fontFamily = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', 'JetBrains Mono', monospace"; }infoBox.appendChild(textBox);return builder;},AddBlod(text) {const boldBox = document.createElement("span");boldBox.innerText = text;boldBox.style.fontWeight = "bold";boldBox.style.marginRight = "13px";infoBox.appendChild(boldBox);return builder;},AddLink: function (text, url) {const linkBox = document.createElement("span");linkBox.innerHTML = `<a target="_blank" href="${url}">${text}</a>`;linkBox.style.marginRight = "13px";infoBox.appendChild(linkBox);return builder;},AddLineBreak: function () {infoBox.appendChild(document.createElement("br"));return builder;},};return builder;},};VCUtil.Stat.Format = {TidMapping: [{ "id": 1, "name": "动画(主分区)" }, { "id": 24, "name": "MAD·AMV" }, { "id": 25, "name": "MMD·3D" }, { "id": 47, "name": "短片·手书" }, { "id": 257, "name": "配音" }, { "id": 210, "name": "手办·模玩" }, { "id": 86, "name": "特摄" }, { "id": 253, "name": "动漫杂谈" }, { "id": 27, "name": "综合" }, { "id": 13, "name": "番剧(主分区)" }, { "id": 51, "name": "资讯" }, { "id": 152, "name": "官方延伸" }, { "id": 32, "name": "完结动画" }, { "id": 33, "name": "连载动画" }, { "id": 167, "name": "国创(主分区)" }, { "id": 153, "name": "##动画" }, { "id": 168, "name": "##原创相关" }, { "id": 169, "name": "布袋戏" }, { "id": 170, "name": "资讯" }, { "id": 195, "name": "动态漫·广播剧" }, { "id": 3, "name": "音乐(主分区)" }, { "id": 28, "name": "原创音乐" }, { "id": 31, "name": "翻唱" }, { "id": 30, "name": "VOCALOID·UTAU" }, { "id": 59, "name": "演奏" }, { "id": 193, "name": "MV" }, { "id": 29, "name": "音乐现场" }, { "id": 130, "name": "音乐综合" }, { "id": 243, "name": "乐评盘点" }, { "id": 244, "name": "音乐教学" }, { "id": 194, "name": "电音(已下线)" }, { "id": 129, "name": "舞蹈(主分区)" }, { "id": 20, "name": "宅舞" }, { "id": 154, "name": "舞蹈综合" }, { "id": 156, "name": "舞蹈教程" }, { "id": 198, "name": "街舞" }, { "id": 199, "name": "明星舞蹈" }, { "id": 200, "name": "国风舞蹈" }, { "id": 255, "name": "手势·网红舞" }, { "id": 4, "name": "游戏(主分区)" }, { "id": 17, "name": "单机游戏" }, { "id": 171, "name": "电子竞技" }, { "id": 172, "name": "手机游戏" }, { "id": 65, "name": "网络游戏" }, { "id": 173, "name": "桌游棋牌" }, { "id": 121, "name": "GMV" }, { "id": 136, "name": "音游" }, { "id": 19, "name": "Mugen" }, { "id": 36, "name": "知识(主分区)" }, { "id": 201, "name": "科学科普" }, { "id": 124, "name": "社科·法律·心理(原社科人文、原趣味科普人文)" }, { "id": 228, "name": "人文历史" }, { "id": 207, "name": "财经商业" }, { "id": 208, "name": "校园学习" }, { "id": 209, "name": "职业职场" }, { "id": 229, "name": "设计·创意" }, { "id": 122, "name": "野生技术协会" }, { "id": 39, "name": "演讲·公开课(已下线)" }, { "id": 96, "name": "星海(已下线)" }, { "id": 98, "name": "机械(已下线)" }, { "id": 188, "name": "科技(主分区)" }, { "id": 95, "name": "数码(原手机平板)" }, { "id": 230, "name": "软件应用" }, { "id": 231, "name": "计算机技术" }, { "id": 232, "name": "科工机械 (原工业·工程·机械)" }, { "id": 233, "name": "极客DIY" }, { "id": 189, "name": "电脑装机(已下线)" }, { "id": 190, "name": "摄影摄像(已下线)" }, { "id": 191, "name": "影音智能(已下线)" }, { "id": 234, "name": "运动(主分区)" }, { "id": 235, "name": "篮球" }, { "id": 249, "name": "足球" }, { "id": 164, "name": "健身" }, { "id": 236, "name": "竞技体育" }, { "id": 237, "name": "运动文化" }, { "id": 238, "name": "运动综合" }, { "id": 223, "name": "汽车(主分区)" }, { "id": 258, "name": "汽车知识科普" }, { "id": 245, "name": "赛车" }, { "id": 246, "name": "改装玩车" }, { "id": 247, "name": "新能源车" }, { "id": 248, "name": "房车" }, { "id": 240, "name": "摩托车" }, { "id": 227, "name": "购车攻略" }, { "id": 176, "name": "汽车生活" }, { "id": 224, "name": "汽车文化(已下线)" }, { "id": 225, "name": "汽车极客(已下线)" }, { "id": 226, "name": "智能出行(已下线)" }, { "id": 160, "name": "生活(主分区)" }, { "id": 138, "name": "搞笑" }, { "id": 250, "name": "出行" }, { "id": 251, "name": "三农" }, { "id": 239, "name": "家居房产" }, { "id": 161, "name": "手工" }, { "id": 162, "name": "绘画" }, { "id": 21, "name": "日常" }, { "id": 254, "name": "亲子" }, { "id": 76, "name": "美食圈(重定向)" }, { "id": 75, "name": "动物圈(重定向)" }, { "id": 163, "name": "运动(重定向)" }, { "id": 176, "name": "汽车(重定向)" }, { "id": 174, "name": "其他(已下线)" }, { "id": 211, "name": "美食(主分区)" }, { "id": 76, "name": "美食制作(原[生活]->[美食圈])" }, { "id": 212, "name": "美食侦探" }, { "id": 213, "name": "美食测评" }, { "id": 214, "name": "田园美食" }, { "id": 215, "name": "美食记录" }, { "id": 217, "name": "动物圈(主分区)" }, { "id": 218, "name": "喵星人" }, { "id": 219, "name": "汪星人" }, { "id": 220, "name": "动物二创" }, { "id": 221, "name": "野生动物" }, { "id": 222, "name": "小宠异宠" }, { "id": 75, "name": "动物综合" }, { "id": 119, "name": "鬼畜(主分区)" }, { "id": 22, "name": "鬼畜调教" }, { "id": 26, "name": "音MAD" }, { "id": 126, "name": "人力VOCALOID" }, { "id": 216, "name": "鬼畜剧场" }, { "id": 127, "name": "教程演示" }, { "id": 155, "name": "时尚(主分区)" }, { "id": 157, "name": "美妆护肤" }, { "id": 252, "name": "仿妆cos" }, { "id": 158, "name": "穿搭" }, { "id": 159, "name": "时尚潮流" }, { "id": 164, "name": "健身(重定向)" }, { "id": 192, "name": "风尚标(已下线)" }, { "id": 202, "name": "资讯(主分区)" }, { "id": 203, "name": "热点" }, { "id": 204, "name": "环球" }, { "id": 205, "name": "##" }, { "id": 206, "name": "综合" }, { "id": 165, "name": "广告(主分区)" }, { "id": 166, "name": "广告(已下线)" }, { "id": 5, "name": "娱乐(主分区)" }, { "id": 71, "name": "综艺" }, { "id": 241, "name": "娱乐杂谈" }, { "id": 242, "name": "粉丝创作" }, { "id": 137, "name": "明星综合" }, { "id": 131, "name": "Korea相关(已下线)" }, { "id": 181, "name": "影视(主分区)" }, { "id": 182, "name": "影视杂谈" }, { "id": 183, "name": "影视剪辑" }, { "id": 85, "name": "小剧场" }, { "id": 184, "name": "预告·资讯" }, { "id": 256, "name": "短片" }, { "id": 177, "name": "纪录片(主分区)" }, { "id": 37, "name": "人文·历史" }, { "id": 178, "name": "科学·探索·自然" }, { "id": 179, "name": "#事" }, { "id": 180, "name": "##·美食·旅行" }, { "id": 23, "name": "电影(主分区)" }, { "id": 147, "name": "华语电影" }, { "id": 145, "name": "欧美电影" }, { "id": 146, "name": "日本电影" }, { "id": 83, "name": "其他国家" }, { "id": 11, "name": "电视剧(主分区)" }, { "id": 185, "name": "##剧" }, { "id": 187, "name": "海外剧" }],HumanReadableNumber: function (num) {var r###lt = '', counter = 0;num = (num || 0).toString();for (var i = num.length - 1; i >= 0; i--) {counter++;r###lt = num.charAt(i) + r###lt;if (!(counter % 3) && i != 0) { r###lt = ',' + r###lt; }}return r###lt;},VocaloidAchievement: function (plays) {if (plays >= 10000000) return "神话";if (plays >= 5000000) return "申舌";if (plays >= 1000000) return "传说";if (plays >= 500000) return "专兑";if (plays >= 100000) return "殿堂";return "待成就";},Tname: function (tid) {return VCUtil.Stat.Format.TidMapping.find(e => e.id === tid)?.name || "未知分区";},Timezone: function (timestamp, timezone) {return new Date(timestamp * 1000).toLocaleString('sv-SE', {timeZone: timezone});}};VCUtil.Entry = () => {const urlInfo = VCUtil.URL.Info();if (urlInfo === null) return;const flag = VCUtil.ConfigFlags[urlInfo.type];if (flag.stdurl) VCUtil.URL.Standardize(urlInfo);if (flag.stdurlAll) VCUtil.URL.StandardizeAllUrl();if (flag.stat) VCUtil.Stat.FetchData(urlInfo.avid, urlInfo.part);}VCUtil.MenuCommand = {MenuCommands: [],TimezonesMapping: [{ IATA: "PVG", timezone: "Asia/Shanghai" },{ IATA: "NRT", timezone: "Asia/Tokyo" },{ IATA: "LAX", timezone: "America/Los_Angeles" },{ IATA: "JFK", timezone: "America/New_York" },{ IATA: "LHR", timezone: "Europe/London" },{ IATA: "MUC", timezone: "Europe/Berlin" },{ IATA: "KIV", timezone: "Europe/Kiev" },{ IATA: "SVO", timezone: "Europe/Moscow" },{ IATA: "SYD", timezone: "Australia/Sydney" },{ IATA: "UNIX", timezone: "Timestamp" },],CurrentAndNextTimezone: function (slotId) {const currentIATA = GM_getValue(`VCUtil_Timezone_${slotId}`, VCUtil.ConfigFlags.timezoneSlotDefault[slotId]);const currentIndex = VCUtil.MenuCommand.TimezonesMapping.findIndex(e => e.IATA === currentIATA);const current = VCUtil.MenuCommand.TimezonesMapping[currentIndex];console.log(`[B站视频统计] [Menu] 时区槽位 ${slotId + 1} 当前`, current);const nextIndex = (currentIndex + 1) % VCUtil.MenuCommand.TimezonesMapping.length;const next = VCUtil.MenuCommand.TimezonesMapping[nextIndex];console.log(`[B站视频统计] [Menu] 时区槽位 ${slotId + 1} 下个`, next);return { current: current, next: next };},ChangeTimezoneSetting: function (slotId) {const { next } = VCUtil.MenuCommand.CurrentAndNextTimezone(slotId);GM_setValue(`VCUtil_Timezone_${slotId}`, next.IATA);VCUtil.MenuCommand.Register();console.log(`[B站视频统计] [Menu] 时区槽位 ${slotId + 1} 设置成功`);},Register: function () {VCUtil.MenuCommand.MenuCommands.forEach(meunCommandId => GM_unregisterMenuCommand(meunCommandId));VCUtil.MenuCommand.MenuCommands = [];for (let slotId = 0; slotId < VCUtil.ConfigFlags.timezoneSlotsCount; slotId++) {const { current, next } = VCUtil.MenuCommand.CurrentAndNextTimezone(slotId);const meunCommandId = GM_registerMenuCommand(`切换槽位 ${slotId + 1} 时区 ${current.IATA} -> ${next.IATA}`,() => VCUtil.MenuCommand.ChangeTimezoneSetting(slotId),);VCUtil.MenuCommand.MenuCommands.push(meunCommandId);}}};(() => {'use strict';console.log("[B站视频统计] 脚本已加载");console.log("[B站视频统计] 功能开关:", VCUtil.ConfigFlags);const observer = new MutationObserver((_mutationList, _observed) => VCUtil.Entry());const el = document.querySelector('#app');console.log("[B站视频统计] 挂载到元素:", el);observer.observe(el, { childList: true, subtree: true });VCUtil.MenuCommand.Register();})();