// ==UserScript==
// @name B站自动点赞
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 智能自动点赞:支持开关控制、动态点赞时机、防止重复点赞
// @author ycl
// @match https://www.bilibili.com/video/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/561638/B%E7%AB%99%E8%87%AA%E5%8A%A8%E7%82%B9%E8%B5%9E.user.js
// @updateURL https://update.greasyfork.icu/scripts/561638/B%E7%AB%99%E8%87%AA%E5%8A%A8%E7%82%B9%E8%B5%9E.meta.js
// ==/UserScript==
(function() {
'use strict';
// ========== 配置 ==========
const CONFIG = {
checkInterval: 1000,
initDelay: 2000,
shortVideoThreshold: 60,
shortVideoProgress: 2/3,
longVideoTime: 60,
};
// ========== 状态管理 ==========
let state = {
enabled: true,
currentVideoId: null,
hasLiked: false,
manuallyUnliked: false,
likeChecked: false,
checkTimer: null,
};
// ========== 工具函数 ==========
// 获取当前视频ID(从URL或页面元素)
function getVideoId() {
const match = window.location.pathname.match(/\/video\/(BV[\w]+|av\d+)/);
return match ? match[1] : null;
}
// 获取点赞按钮
function getLikeButton() {
return document.querySelector('#arc_toolbar_report .video-like') ||
document.querySelector('.video-toolbar-left .video-like');
}
// 获取视频元素
function getVideoElement() {
return document.querySelector('#bilibili-player video') ||
document.querySelector('.bpx-player-video-wrap video');
}
// 检查是否已点赞
function isLiked() {
const likeBtn = getLikeButton();
return likeBtn && likeBtn.classList.contains('on');
}
// 获取视频总时长
function getVideoDuration() {
const video = getVideoElement();
return video ? video.duration : 0;
}
// 获取当前播放时间
function getCurrentTime() {
const video = getVideoElement();
return video ? video.currentTime : 0;
}
// ========== 核心逻辑 ==========
// 检查是否应该点赞
function shouldLike() {
if (!state.enabled) return false;
if (!state.likeChecked) return false; // 必须先完成初始状态检查
if (state.hasLiked) return false;
if (state.manuallyUnliked) return false;
if (isLiked()) return false;
const duration = getVideoDuration();
const currentTime = getCurrentTime();
if (duration <= 0 || currentTime <= 0) return false;
// 短视频(小于1分钟):播放进度达到2/3
if (duration < CONFIG.shortVideoThreshold) {
return currentTime >= duration * CONFIG.shortVideoProgress;
}
// 长视频(大于等于1分钟):播放超过1分钟
return currentTime >= CONFIG.longVideoTime;
}
// 执行点赞
function doLike() {
const likeBtn = getLikeButton();
if (likeBtn && !isLiked()) {
likeBtn.click();
state.hasLiked = true;
console.log('[B站自动点赞] 已自动点赞');
updateUI();
}
}
// 定时检查
function checkAndLike() {
if (shouldLike()) {
doLike();
}
}
// 重置视频状态
function resetVideoState() {
const newVideoId = getVideoId();
if (newVideoId !== state.currentVideoId) {
console.log('[B站自动点赞] 检测到视频切换:', newVideoId);
state.currentVideoId = newVideoId;
state.hasLiked = false;
state.manuallyUnliked = false;
state.likeChecked = false;
updateUI();
}
// 等待视频数据加载完成后再检查点赞状态
// 条件:视频已开始播放(currentTime > 0)说明数据已加载
if (!state.likeChecked) {
const video = getVideoElement();
const likeBtn = getLikeButton();
if (video && video.currentTime > 0 && likeBtn) {
state.likeChecked = true;
if (isLiked()) {
state.hasLiked = true;
console.log('[B站自动点赞] 检测到页面已点赞');
}
}
}
}
// ========== 点赞状态监听 ==========
// 监听点赞按钮变化(检测用户手动取消点赞)
function setupLikeObserver() {
const likeBtn = getLikeButton();
if (!likeBtn) return;
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'class') {
const wasLiked = state.hasLiked;
const nowLiked = isLiked();
// 如果之前自动点赞过,现在取消了,标记为手动取消
if (wasLiked && !nowLiked) {
state.manuallyUnliked = true;
console.log('[B站自动点赞] 检测到手动取消点赞,不再自动点赞此视频');
updateUI();
}
}
}
});
observer.observe(likeBtn, { attributes: true });
return observer;
}
// ========== UI控制面板 ==========
let panelState = {
isDragging: false,
startX: 0,
startY: 0,
currentX: 0,
currentY: 0,
};
function createControlPanel() {
// 检查是否已存在
if (document.getElementById('auto-like-panel')) return;
const panel = document.createElement('div');
panel.id = 'auto-like-panel';
panel.innerHTML = `
✕
`;
// 读取保存的位置
loadPanelPosition(panel);
// 设置事件监听
setupDragEvents(panel);
// 关闭按钮点击事件
const closeBtn = panel.querySelector('.close-btn');
closeBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡,避免触发拖动
panel.style.display = 'none';
console.log('[B站自动点赞] 已隐藏,刷新页面后重新显示');
});
document.body.appendChild(panel);
updateUI();
}
function loadPanelPosition(panel) {
if (typeof GM_getValue === 'function') {
panelState.currentX = GM_getValue('panelX', window.innerWidth - 60);
panelState.currentY = GM_getValue('panelY', 200);
} else {
panelState.currentX = window.innerWidth - 60;
panelState.currentY = 200;
}
panel.style.left = panelState.currentX + 'px';
panel.style.top = panelState.currentY + 'px';
}
function savePanelPosition() {
if (typeof GM_setValue === 'function') {
GM_setValue('panelX', panelState.currentX);
GM_setValue('panelY', panelState.currentY);
}
}
function setupDragEvents(panel) {
let hasMoved = false;
let startTime = 0;
panel.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
panelState.isDragging = true;
hasMoved = false;
startTime = Date.now();
const rect = panel.getBoundingClientRect();
panelState.startX = e.clientX;
panelState.startY = e.clientY;
panelState.currentX = rect.left;
panelState.currentY = rect.top;
panel.classList.add('dragging');
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!panelState.isDragging) return;
const deltaX = e.clientX - panelState.startX;
const deltaY = e.clientY - panelState.startY;
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
hasMoved = true;
}
let newX = panelState.currentX + deltaX;
let newY = panelState.currentY + deltaY;
// 限制范围(避开滚动条)
const maxX = document.documentElement.clientWidth - panel.offsetWidth;
const maxY = document.documentElement.clientHeight - panel.offsetHeight;
newX = Math.max(0, Math.min(newX, maxX));
newY = Math.max(0, Math.min(newY, maxY));
panel.style.left = newX + 'px';
panel.style.top = newY + 'px';
});
document.addEventListener('mouseup', (e) => {
if (!panelState.isDragging) return;
panelState.isDragging = false;
panel.classList.remove('dragging');
const rect = panel.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const screenCenter = window.innerWidth / 2;
// 自动贴边(右侧留出滚动条位置)
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
if (centerX < screenCenter) {
panelState.currentX = 0;
} else {
panelState.currentX = document.documentElement.clientWidth - panel.offsetWidth;
}
panelState.currentY = rect.top;
panel.style.left = panelState.currentX + 'px';
savePanelPosition();
// 如果没有移动且点击时间短,视为点击切换开关
const clickDuration = Date.now() - startTime;
if (!hasMoved && clickDuration < 200) {
toggleEnabled();
}
});
panel.addEventListener('selectstart', (e) => e.preventDefault());
}
function updateUI() {
const panel = document.getElementById('auto-like-panel');
if (!panel) return;
if (!state.enabled) {
panel.classList.add('disabled');
} else {
panel.classList.remove('disabled');
}
}
function toggleEnabled() {
state.enabled = !state.enabled;
// 保存状态
if (typeof GM_setValue === 'function') {
GM_setValue('autoLikeEnabled', state.enabled);
}
console.log('[B站自动点赞]', state.enabled ? '已开启' : '已关闭');
updateUI();
}
// ========== URL变化监听(SPA路由) ==========
function setupUrlObserver() {
let lastUrl = location.href;
// 使用 MutationObserver 监听 URL 变化
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
resetVideoState();
}
});
observer.observe(document.body, { childList: true, subtree: true });
// 监听 popstate 事件(浏览器前进/后退)
window.addEventListener('popstate', resetVideoState);
// 监听 pushState 和 replaceState
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
resetVideoState();
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
resetVideoState();
};
}
// ========== 初始化 ==========
function init() {
console.log('[B站自动点赞] 初始化...');
// 读取保存的开关状态
if (typeof GM_getValue === 'function') {
state.enabled = GM_getValue('autoLikeEnabled', true);
}
// 设置当前视频ID
state.currentVideoId = getVideoId();
// 创建控制面板
createControlPanel();
// 设置点赞状态监听
setupLikeObserver();
setupUrlObserver();
state.checkTimer = setInterval(() => {
resetVideoState();
checkAndLike();
}, CONFIG.checkInterval);
console.log('[B站自动点赞] 初始化完成,当前状态:', state.enabled ? '开启' : '关闭');
}
// 等待页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(init, CONFIG.initDelay);
});
} else {
setTimeout(init, CONFIG.initDelay);
}
})();