// ==UserScript== // @name b站重复观看计数器2.0(极简/次数/日期/预估观看时间) // @icon https://www.bilibili.com/favicon.ico // @version 1.13.8 // @description Emoji播放次数 + 日期 次数 + 预估观看完成时间 // @author 你 // @match *://*.bilibili.com/video/* // @match *://*.bilibili.com/video/*?* // @grant GM_getValue // @grant GM_setValue // @run-at document-body // ==/UserScript== (function() { 'use strict'; // 工具函数 function getToday() { return new Date().toISOString().split('T')[0]; } // 获取简写日期格式:26/4/26 function getShortDate(dateStr = null) { const date = dateStr ? new Date(dateStr) : new Date(); const year = date.getFullYear().toString().slice(-2); const month = date.getMonth() + 1; const day = date.getDate(); return `${year}/${month}/${day}`; } // 获取视频历史 function getVideoHistory(bvId) { try { return JSON.parse(GM_getValue(`${bvId}_history`, '{}')); } catch { return {}; } } // 更新历史记录 function updateVideoHistory(bvId) { const today = getToday(); const history = getVideoHistory(bvId); history[today] = (history[today] || 0) + 1; GM_setValue(`${bvId}_history`, JSON.stringify(history)); return history; } // 计算总次数 function getTotalCount(history) { return Object.values(history).reduce((sum, count) => sum + count, 0); } // 获取最近两天记录 function getRecentTwoDays(history) { const dates = Object.keys(history).sort().reverse(); const result = []; for (let i = 0; i < Math.min(2, dates.length); i++) { const date = dates[i]; const shortDate = getShortDate(date); result.push(`${shortDate} ${history[date]}次`); } return result; } // 可靠的时长获取函数 function parseVideoDuration() { console.log('🎯 开始查找视频时长...'); // 方法1:从播放器控件获取 const durationSelectors = [ '.bpx-player-ctrl-time-duration', '.bilibili-player-video-time-total', '.bpx-player-video-duration', '.bpx-player-duration-time', '.duration-text', '.video-duration', '[class*="duration"]', '[class*="time-total"]' ]; for (let selector of durationSelectors) { const elements = document.querySelectorAll(selector); for (let element of elements) { const text = (element.textContent || element.innerText || '').trim(); if (text && /^(\d+:)+\d+$/.test(text)) { console.log(`✅ 通过选择器 ${selector} 找到时长: ${text}`); return text; } } } // 方法2:从视频信息中获取 const infoSelectors = ['.video-data', '.video-info', '.video-desc']; for (let selector of infoSelectors) { const element = document.querySelector(selector); if (element) { const text = element.textContent || ''; const match = text.match(/(\d+):(\d+)/); if (match) { const duration = `${match[1]}:${match[2]}`; console.log(`✅ 从信息区找到时长: ${duration}`); return duration; } } } // 方法3:尝试从video元素获取 const video = document.querySelector('video'); if (video && video.duration) { const minutes = Math.floor(video.duration / 60); const seconds = Math.floor(video.duration % 60); const duration = `${minutes}:${seconds.toString().padStart(2, '0')}`; console.log(`✅ 从video元素获取时长: ${duration}`); return duration; } console.log('❌ 未找到视频时长'); return null; } // 将时长文本转换为秒数 function durationToSeconds(durationText) { if (!durationText) return null; try { const parts = durationText.split(':').map(Number); if (parts.length === 3) { // HH:MM:SS return parts[0] * 3600 + parts[1] * 60 + parts[2]; } else if (parts.length === 2) { // MM:SS return parts[0] * 60 + parts[1]; } else if (parts.length === 1) { // SS return parts[0]; } } catch (error) { console.log('❌ 解析时长失败:', error); } return null; } // 计算预计结束时间 function calculateEndTime(durationSeconds) { if (!durationSeconds) return '--:--'; try { const now = new Date(); const endTime = new Date(now.getTime() + durationSeconds * 1000); const hours = endTime.getHours().toString().padStart(2, '0'); const minutes = endTime.getMinutes().toString().padStart(2, '0'); console.log(`⏳ 计算: 当前${now.getHours()}:${now.getMinutes()}, 视频${durationSeconds}秒, 预计${hours}:${minutes}结束`); return `${hours}:${minutes}`; } catch (error) { console.log('❌ 计算结束时间失败:', error); return '--:--'; } } // 查找"未经作者授权"行 function findCopyrightLine() { const copyrightSelectors = [ '.copyright-info', '.copyright-area', '.copyright', '.copyright-text', '[class*="copyright"]', '[class*="ban-reprint"]', '[class*="unauthorized"]', '.video-desc .desc', '.video-desc-info', '.desc-info' ]; for (let selector of copyrightSelectors) { const elements = document.querySelectorAll(selector); for (let element of elements) { const text = element.textContent || ''; if (text.includes('未经作者授权') || text.includes('禁止转载') || text.includes('未经授权')) { return element; } } } return null; } // 主显示函数 function showCounter(retryCount = 0) { const url = window.location.href; const bvMatch = url.match(/\/(BV[a-zA-Z0-9]+)/); if (!bvMatch) return; const bvId = bvMatch[1]; // 更新记录 const history = updateVideoHistory(bvId); const totalCount = getTotalCount(history); const today = getToday(); const todayCount = history[today] || 0; // 获取最近两天记录 const recentDays = getRecentTwoDays(history); const todayShort = getShortDate(); // 获取视频时长和预计时间 const durationText = parseVideoDuration(); const durationSeconds = durationToSeconds(durationText); const endTime = calculateEndTime(durationSeconds); // 如果没找到时长且重试次数少于3次,延迟重试 if (!durationText && retryCount < 3) { console.log(`⏳ 第${retryCount + 1}次重试查找时长...`); setTimeout(() => showCounter(retryCount + 1), 1000); return; } console.log(`🎬 ${bvId}: 时长${durationText || '未知'}, 预计${endTime}结束`); // 移除旧容器 const old = document.getElementById('bili-counter-container'); if (old) old.remove(); // 创建容器 const container = document.createElement('div'); container.id = 'bili-counter-container'; container.style.cssText = ` display: inline-block; margin-left: 12px; vertical-align: middle; `; // 🎨 计数器设计 - Emoji播放图标 const timeTitle = durationText ? `预计结束时间: ${endTime} (视频时长: ${durationText})` : '视频时长未获取到'; container.innerHTML = `
▶️ ${totalCount} | 📅 ${todayShort} ${todayCount} | ${endTime}
`; // 优先插入到"未经作者授权"行 const copyrightLine = findCopyrightLine(); if (copyrightLine) { copyrightLine.appendChild(container); console.log(`✅ 插入到版权行: ${bvId} 总${totalCount}次, 预计${endTime}结束`); return; } // 备用插入位置 const descSelectors = ['.video-desc', '.desc', '.video-description']; for (let selector of descSelectors) { const desc = document.querySelector(selector); if (desc) { desc.prepend(container); console.log(`✅ 插入到描述区: ${bvId} 总${totalCount}次`); return; } } // 最终备用 container.style.position = 'fixed'; container.style.top = '100px'; container.style.right = '20px'; container.style.zIndex = '9999'; document.body.appendChild(container); } // 延迟执行 setTimeout(() => { console.log('⏳ 启动视频计时器...'); showCounter(); }, 3000); })();