您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
显示b站虚拟主播录播的弹幕密度,方便你找到录播中有趣的爆点。仅对字幕库(https://zimu.bili.studio)有效。视频右上角可以开/关图表
// ==UserScript== // @name 获取虚拟主播录播弹幕密度 // @namespace http://tampermonkey.net/ // @version 2.2 // @description 显示b站虚拟主播录播的弹幕密度,方便你找到录播中有趣的爆点。仅对字幕库(https://zimu.bili.studio)有效。视频右上角可以开/关图表 // @author 开朗小粟 // @match https://zimu-bl6.pages.dev/* // @include https://zimu* // @require https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/Chart.js/3.7.1/chart.js // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; function createToggleButton(videoElement) { // 创建开关外层div const toggleContainer = document.createElement('div'); toggleContainer.style.position = 'absolute'; toggleContainer.style.top = '10px'; toggleContainer.style.right = '10px'; toggleContainer.style.zIndex = '1001'; toggleContainer.style.opacity = '0.5'; // 设置透明度为50% // 创建开关输入 const toggleSwitch = document.createElement('input'); toggleSwitch.setAttribute('type', 'checkbox'); toggleSwitch.id = 'chartToggle'; toggleSwitch.checked = true; // 创建label用于美化开关 const toggleLabel = document.createElement('label'); toggleLabel.setAttribute('for', 'chartToggle'); toggleLabel.innerText = '显示热度'; toggleLabel.style.marginLeft = '5px'; // 状态标签 const statusLabel = document.createElement('span'); statusLabel.id = 'statusLabel'; //statusLabel.innerText = 'Chart Off'; // 默认状态为关闭 statusLabel.style.marginLeft = '10px'; // 添加事件监听器以切换图表显示并更新状态标签 toggleSwitch.addEventListener('change', () => { const chart = document.getElementById('danmakuChart'); if (chart) { if (toggleSwitch.checked) { chart.style.display = 'block'; //statusLabel.innerText = 'Chart On'; } else { chart.style.display = 'none'; //statusLabel.innerText = 'Chart Off'; } } }); // 将开关和标签添加到外层div toggleContainer.appendChild(toggleSwitch); toggleContainer.appendChild(toggleLabel); toggleContainer.appendChild(statusLabel); // 将整个开关组添加到视频元素的父节点 videoElement.parentNode.appendChild(toggleContainer); } // Function to get the URL of the video element by class name function getVideoURLByClass() { let videoElement = document.querySelector('video.art-video'); if (videoElement) { // Add event listener for 'loadeddata' to ensure the video is fully loaded videoElement.addEventListener('loadeddata', function () { let videoURL = videoElement.src; //console.log('Video URL:', videoURL); //alert('Video URL: ' + videoURL); // Replace .mp4 with .xml to get the danmaku file URL let danmakuURL = videoURL.replace('.mp4', '.xml'); //console.log('Danmaku URL:', danmakuURL); //alert('Danmaku URL: ' + danmakuURL); // Fetch the danmaku XML file fetch(danmakuURL) .then(response => response.text()) .then(xmlText => { // Parse the XML text let parser = new DOMParser(); let xmlDoc = parser.parseFromString(xmlText, 'text/xml'); // Process the XML data let interval = 5; let num_chats_per_minute = {}; let d_elements = xmlDoc.getElementsByTagName('d'); for (let d of d_elements) { let send_time = parseFloat(d.getAttribute('p').split(',')[0]); let bound = Math.floor(send_time / interval); if (!(bound in num_chats_per_minute)) { num_chats_per_minute[bound] = 0; } num_chats_per_minute[bound] += 1; } // Fill in missing keys with 0 let maxKey = Math.max(...Object.keys(num_chats_per_minute).map(Number)); let x = []; let y = []; for (let i = 0; i <= maxKey; i++) { x.push(i * interval); y.push(num_chats_per_minute[i] || 0); } //console.log('x:', x); //console.log('y:', y); //alert('x: ' + x); //alert('y: ' + y); // Draw the chart drawChart(videoElement, x, y, videoElement.duration); createToggleButton(videoElement); }) .catch(error => { console.error('Error fetching or parsing danmaku XML:', error); }); }, { once: true }); // Ensure the event listener is only called once // Observe the video element for removal const videoObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.removedNodes.forEach((node) => { if (node.nodeType === 1 && node.contains(videoElement)) { //console.log('Video element removed, restarting observer.'); startObserving(); // Restart observing when video element is removed videoObserver.disconnect(); // Disconnect the video observer } }); }); }); // Start observing the parent node of the video element for child node removals // videoObserver.observe(videoElement.parentNode, { childList: true }); videoObserver.observe(document.body, { childList: true, subtree: true }); } else { console.log('No video element found.'); } } // Function to draw the chart on the video element function drawChart1(videoElement, x, y, videoDuration) { // Calculate chart height as one-tenth of video height const chartHeight = videoElement.clientHeight / 3; // Create a canvas element const canvas = document.createElement('canvas'); canvas.id = 'danmakuChart'; canvas.style.position = 'absolute'; canvas.style.bottom = '0'; canvas.style.left = '0.5%'; // Center the canvas horizontally canvas.style.width = '99%'; canvas.style.height = `${chartHeight}px`; // Set the height to one-tenth of the video height canvas.style.pointerEvents = 'none'; // Allow clicks to pass through canvas.style.zIndex = '1000'; // Ensure the canvas is on top // Append the canvas to the video element's parent videoElement.parentNode.appendChild(canvas); // Adjust x-axis labels to match video duration const adjustedX = []; for (let i = 0; i < x.length; i++) { adjustedX.push((x[i] / (x[x.length - 1])) * videoDuration); } // Draw the chart const ctx = canvas.getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: adjustedX, datasets: [{ data: y, backgroundColor: 'rgba(255, 99, 132, 0.7)', borderColor: 'rgba(255, 99, 132, 1)', borderWidth: 0, pointRadius: 0, pointHoverRadius: 0, fill: true }] }, options: { responsive: false, maintainAspectRatio: false, plugins: { legend: { display: false }, title: { display: false } }, scales: { x: { type: 'linear', display: true, position: 'bottom', min: 0, max: videoDuration, ticks: { display: false }, grid: { display: false } }, y: { display: false, ticks: { display: false }, grid: { display: false } } }, layout: { padding: { top: 0, left: 0, right: 0, bottom: 0 } }, elements: { line: { tension: 0.4 // smooth lines } } } }); } function getRandomColor() { const l = 20; const r = Math.floor(l + Math.random() * (255 - l)); // 随机生成0-255之间的整数 const g = Math.floor(l + Math.random() * (255 - l)); // 随机生成0-255之间的整数 const b = Math.floor(l + Math.random() * (255 - l)); // 随机生成0-255之间的整数 const a = 0.9; return `rgba(${r}, ${g}, ${b}, ${a})`; } function drawChart(videoElement, x, y, videoDuration) { const chartHeight = videoElement.clientHeight / 3; const canvas = document.createElement('canvas'); canvas.id = 'danmakuChart'; canvas.style.position = 'absolute'; canvas.style.left = '0.5%'; // Center the canvas horizontally canvas.style.width = '99%'; canvas.style.height = `${chartHeight}px`; canvas.style.pointerEvents = 'none'; // Allow clicks to pass through canvas.style.zIndex = '1000'; // Ensure the canvas is on top const parentDiv = videoElement.parentNode; videoElement.parentNode.appendChild(canvas); // Append the canvas initially window.cnt = 0; // Function to update canvas position based on class 'art-control-show' let lastClass = parentDiv.className; // Keep track of the last class const progressElement = document.querySelector('.art-progress'); let p1 = canvas.parentElement.getBoundingClientRect().bottom let p2 = progressElement.getBoundingClientRect().top let b1 = `${p1 - p2}px`; let b2 = '0'; function updateCanvasPosition() { if (parentDiv.classList.contains('art-control-show')) { //console.log('contain', window.cnt, parentDiv.className); // Place the canvas above the '.art-progress' element if it exists const progressElement = document.querySelector('.art-progress'); if (progressElement) { //progressElement.style.position = 'relative'; // Ensure it's positioned //progressElement.parentNode.insertBefore(canvas, progressElement); //console.log(p1,p2); canvas.style.bottom = b1; } } else { //console.log('Not contain', window.cnt, parentDiv.className); // Place the canvas at the bottom of the video element canvas.style.bottom = b2; parentDiv.appendChild(canvas); } } const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const newClass = parentDiv.className; if (newClass !== lastClass) { const progressElement = document.querySelector('.art-progress'); //console.log(progressElement.getBoundingClientRect()); lastClass = newClass; // Update the tracked class updateCanvasPosition(); // Only update if class actually changed } } }); }); // Start observing the parentDiv for attribute changes observer.observe(parentDiv, { attributes: true, // Monitor attributes attributeFilter: ['class'] // Only monitor the 'class' attribute }); updateCanvasPosition(); // Initial placement check // Adjust x-axis labels to match video duration const adjustedX = x.map(value => (value / x[x.length - 1]) * videoDuration); // Draw the chart const ctx = canvas.getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: adjustedX, datasets: [{ data: y, backgroundColor: getRandomColor(), // borderColor: 'rgba(255, 99, 132, 1)', borderWidth: 0, pointRadius: 0, pointHoverRadius: 0, fill: true }] }, options: { responsive: false, maintainAspectRatio: false, plugins: { legend: { display: false }, title: { display: false } }, scales: { x: { type: 'linear', display: true, position: 'bottom', min: 0, max: videoDuration, ticks: { display: false }, grid: { display: false } }, y: { display: false, ticks: { display: false }, grid: { display: false } } }, layout: { padding: { top: 0, left: 0, right: 0, bottom: 0 } }, elements: { line: { tension: 0.4 } // smooth lines } } }); } // Observer to detect when elements are added to the DOM const observer = new MutationObserver((mutations) => { for (let mutation of mutations) { for (let node of mutation.addedNodes) { if (node.nodeType === 1) { // Only process element nodes let videoElement = document.querySelector('video.art-video'); if (videoElement) { getVideoURLByClass(); observer.disconnect(); // Stop observing after the video element is found return; } } } } }); // Function to start observing the document body for added nodes function startObserving() { observer.observe(document.body, { childList: true, subtree: true }); } // Start observing when the script is first run startObserving(); })();