[JavaScript] 纯文本查看 复制代码
// ==UserScript==
// [url=home.php?mod=space&uid=170990]@name[/url] 快手直播监听(刷新版)
// [url=home.php?mod=space&uid=467642]@namespace[/url] http://tampermonkey.net/
// [url=home.php?mod=space&uid=1248337]@version[/url] 1.0.1
// @description 通过刷新页面检测当前主播是否开播
// [url=home.php?mod=space&uid=686208]@AuThor[/url] DouyinLiveRecorder
// [url=home.php?mod=space&uid=195849]@match[/url] https://live.kuaishou.com/*
// [url=home.php?mod=space&uid=593100]@Icon[/url] https://www.kuaishou.com/favicon.ico
// [url=home.php?mod=space&uid=609072]@grant[/url] GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant unsafeWindow
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置项 ====================
const CONFIG = {
refreshInterval: GM_getValue('refreshInterval', 30000),
autoStart: GM_getValue('autoStart', false),
enableNotification: GM_getValue('enableNotification', true),
notLiveXpath: '//*[@id="app"]/div[3]/div[2]/div[1]/div/div[1]/div/div[1]/div/div/div/div[1]/div/div[1]/div[2]/div/div/div[2]/p/span',
notLiveText: '主播尚未开播',
};
// ==================== 状态变量 ====================
let isMonitoring = GM_getValue('state_monitoring', false);
let monitorStartTime = GM_getValue('state_startTime', null);
let liveStartTime = GM_getValue('state_liveStartTime', null);
let isCurrentlyLive = GM_getValue('state_isLive', false);
let totalLiveDuration = GM_getValue('totalLiveDuration', 0);
let checkCount = GM_getValue('state_checkCount', 0);
let refreshTimer = null;
// ==================== 日志系统 ====================
const logHistory = JSON.parse(GM_getValue('logHistory', '[]'));
const MAX_LOGS = 50;
function addLog(message, type = 'info') {
const time = new Date().toLocaleTimeString();
const logEntry = { time, message, type };
logHistory.unshift(logEntry);
if (logHistory.length > MAX_LOGS) logHistory.pop();
GM_setValue('logHistory', JSON.stringify(logHistory));
console.log(`[快手监听-刷新版] ${time} - ${message}`);
updateLogPanel();
return logEntry;
}
function log(msg) { addLog(msg, 'info'); }
function logSuccess(msg) { addLog(msg, 'success'); }
function logError(msg) { addLog(msg, 'error'); }
function logWarn(msg) { addLog(msg, 'warn'); }
// ==================== 工具函数 ====================
function formatDuration(seconds) {
if (!seconds || seconds < 0) return '0秒';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) return `${hours}时${minutes}分${secs}秒`;
if (minutes > 0) return `${minutes}分${secs}秒`;
return `${secs}秒`;
}
function getMonitorDuration() {
if (!monitorStartTime) return 0;
return Math.floor((Date.now() - monitorStartTime) / 1000);
}
function getLiveDuration() {
if (!liveStartTime) return 0;
return Math.floor((Date.now() - liveStartTime) / 1000);
}
function getElementByXPath(xpath) {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
// ==================== 核心检测 ====================
function checkLiveStatus() {
const element = getElementByXPath(CONFIG.notLiveXpath);
if (element && element.textContent.includes(CONFIG.notLiveText)) {
return { isLive: false, message: '主播尚未开播' };
} else {
const videoPlayer = document.querySelector('video');
const livePlayer = document.querySelector('.player-container, .live-player, [class*="player"]');
if (videoPlayer || livePlayer) {
return { isLive: true, message: '检测到直播画面' };
} else {
return { isLive: false, message: '页面加载中...' };
}
}
}
function doCheck() {
checkCount++;
GM_setValue('state_checkCount', checkCount);
updateDisplay();
const result = checkLiveStatus();
if (result.isLive) {
if (!isCurrentlyLive) {
isCurrentlyLive = true;
liveStartTime = Date.now();
GM_setValue('state_isLive', true);
GM_setValue('state_liveStartTime', liveStartTime);
logSuccess(`🔴 【开播】${result.message}`);
showNotification('🔴 主播开播了!', '直播已开始');
stopRefresh();
updateStatusIndicator(true);
} else {
log(`🔴 直播中... ${result.message}`);
}
} else {
if (isCurrentlyLive) {
const sessionLive = getLiveDuration();
totalLiveDuration += sessionLive;
GM_setValue('totalLiveDuration', totalLiveDuration);
logWarn(`⚫ 【下播】本次直播: ${formatDuration(sessionLive)}`);
isCurrentlyLive = false;
liveStartTime = null;
GM_setValue('state_isLive', false);
GM_setValue('state_liveStartTime', null);
}
log(`⚪ 【未开播】${result.message},${CONFIG.refreshInterval/1000}秒后刷新...`);
updateStatusIndicator(false);
scheduleRefresh();
}
}
function scheduleRefresh() {
if (refreshTimer) clearTimeout(refreshTimer);
refreshTimer = setTimeout(() => {
log('🔄 刷新页面...');
location.reload();
}, CONFIG.refreshInterval);
}
function stopRefresh() {
if (refreshTimer) {
clearTimeout(refreshTimer);
refreshTimer = null;
}
}
// ==================== 监听控制 ====================
function startMonitor() {
if (isMonitoring) {
logWarn('监听已在运行中');
return;
}
isMonitoring = true;
monitorStartTime = Date.now();
checkCount = 0;
GM_setValue('state_monitoring', true);
GM_setValue('state_startTime', monitorStartTime);
GM_setValue('state_checkCount', 0);
logSuccess(`▶️ 开始监听,刷新间隔: ${CONFIG.refreshInterval/1000}秒`);
log(`📌 当前页面: ${location.href}`);
createPanel();
updateDisplay();
updateStatusIndicator(false);
setTimeout(doCheck, 2000);
}
function stopMonitor() {
stopRefresh();
if (monitorStartTime) {
const sessionDuration = getMonitorDuration();
logSuccess(`⏹️ 停止监听,监听时长: ${formatDuration(sessionDuration)}`);
}
if (isCurrentlyLive && liveStartTime) {
const sessionLive = getLiveDuration();
totalLiveDuration += sessionLive;
GM_setValue('totalLiveDuration', totalLiveDuration);
log(`📊 保存开播时长: ${formatDuration(sessionLive)}`);
}
isMonitoring = false;
isCurrentlyLive = false;
monitorStartTime = null;
liveStartTime = null;
GM_setValue('state_monitoring', false);
GM_setValue('state_startTime', null);
GM_setValue('state_isLive', false);
GM_setValue('state_liveStartTime', null);
updateStatusIndicator(false, true);
}
function resetStats() {
totalLiveDuration = 0;
checkCount = 0;
GM_setValue('totalLiveDuration', 0);
GM_setValue('state_checkCount', 0);
updateDisplay();
log('🔄 统计已重置');
}
function showNotification(title, text) {
if (CONFIG.enableNotification && typeof GM_notification !== 'undefined') {
GM_notification({ title, text, timeout: 5000 });
}
}
// ==================== UI面板 ====================
let panel = null;
let displayInterval = null;
function createPanel() {
if (panel) return;
panel = document.createElement('div');
panel.id = 'ks-refresh-panel';
panel.innerHTML = `
<style>
#ks-refresh-panel {
position: fixed;
top: 100px;
right: 20px;
width: 320px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #fff;
overflow: hidden;
}
#ks-refresh-panel .panel-header {
padding: 12px 15px;
background: rgba(255,107,53,0.9);
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
font-weight: bold;
font-size: 14px;
}
#ks-refresh-panel .close-btn { cursor: pointer; font-size: 18px; opacity: 0.8; }
#ks-refresh-panel .close-btn:hover { opacity: 1; }
#ks-refresh-panel .panel-body { padding: 15px; }
#ks-refresh-panel .status-section {
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 12px;
margin-bottom: 15px;
}
#ks-refresh-panel .status-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
}
#ks-refresh-panel .status-row .label { color: #888; }
#ks-refresh-panel .status-row .value { color: #4CAF50; font-weight: bold; }
#ks-refresh-panel .status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
#ks-refresh-panel .status-dot.monitoring { background: #ffc107; animation: blink 1s infinite; }
#ks-refresh-panel .status-dot.live { background: #ff4444; animation: pulse-glow 1s infinite; }
#ks-refresh-panel .status-dot.stopped { background: #666; }
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
@keyframes pulse-glow { 0%,100% { box-shadow: 0 0 5px #ff4444; } 50% { box-shadow: 0 0 15px #ff4444; } }
#ks-refresh-panel .btn-row { display: flex; gap: 8px; margin-bottom: 12px; }
#ks-refresh-panel button {
flex: 1;
padding: 10px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
transition: all 0.2s;
}
#ks-refresh-panel .btn-start { background: #4CAF50; color: #fff; }
#ks-refresh-panel .btn-start:hover { background: #45a049; }
#ks-refresh-panel .btn-stop { background: #f44336; color: #fff; }
#ks-refresh-panel .btn-stop:hover { background: #d32f2f; }
#ks-refresh-panel .btn-secondary { background: #333; color: #fff; }
#ks-refresh-panel .btn-secondary:hover { background: #444; }
#ks-refresh-panel .settings-section {
background: rgba(255,255,255,0.05);
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
}
#ks-refresh-panel .settings-label { font-size: 12px; color: #888; margin-bottom: 8px; }
#ks-refresh-panel .interval-control { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
#ks-refresh-panel .interval-control input {
width: 70px;
padding: 6px 8px;
background: #0f0f1a;
border: 1px solid #333;
border-radius: 4px;
color: #fff;
font-size: 14px;
}
#ks-refresh-panel .interval-unit { color: #888; font-size: 13px; }
#ks-refresh-panel .btn-apply {
padding: 6px 12px;
background: #ff6b35;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
#ks-refresh-panel .btn-apply:hover { background: #e55a2b; }
#ks-refresh-panel .interval-presets { display: flex; gap: 6px; }
#ks-refresh-panel .interval-presets button {
flex: 1;
padding: 5px 8px;
background: #222;
color: #aaa;
border: 1px solid #333;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
}
#ks-refresh-panel .interval-presets button:hover {
background: #333;
color: #fff;
border-color: #ff6b35;
}
#ks-refresh-panel .log-section { background: #0a0a12; border-radius: 8px; margin-top: 10px; }
#ks-refresh-panel .log-header {
padding: 8px 12px;
font-size: 12px;
color: #888;
border-bottom: 1px solid #222;
display: flex;
justify-content: space-between;
}
#ks-refresh-panel .log-content {
padding: 8px;
font-size: 11px;
font-family: 'Consolas', monospace;
line-height: 1.6;
max-height: 150px;
min-height: 80px;
overflow-y: auto;
}
#ks-refresh-panel .log-entry { margin: 2px 0; }
#ks-refresh-panel .log-entry.info { color: #888; }
#ks-refresh-panel .log-entry.success { color: #4CAF50; }
#ks-refresh-panel .log-entry.error { color: #f44336; }
#ks-refresh-panel .log-entry.warn { color: #ff9800; }
#ks-refresh-panel .log-time { color: #555; margin-right: 8px; }
</style>
<div class="panel-header">
<span>🔄 快手直播监听(刷新版)</span>
<span class="close-btn">✕</span>
</div>
<div class="panel-body">
<div class="status-section">
<div class="status-row">
<span class="label">状态</span>
<span class="value"><span class="status-dot stopped" id="ks-status-dot"></span><span id="ks-status-text">未启动</span></span>
</div>
<div class="status-row">
<span class="label">监听时长</span>
<span class="value" id="ks-monitor-duration">0秒</span>
</div>
<div class="status-row">
<span class="label">本次开播</span>
<span class="value" id="ks-live-duration">0秒</span>
</div>
<div class="status-row">
<span class="label">累计开播</span>
<span class="value" id="ks-total-live">${formatDuration(totalLiveDuration)}</span>
</div>
<div class="status-row">
<span class="label">刷新次数</span>
<span class="value" id="ks-check-count">${checkCount}</span>
</div>
</div>
<div class="btn-row">
<button class="btn-start">▶ 开始监听</button>
<button class="btn-stop">⏹ 停止</button>
</div>
<div class="btn-row">
<button class="btn-secondary">🔄 立即刷新</button>
<button class="btn-secondary">📊 重置统计</button>
</div>
<div class="settings-section">
<div class="settings-label">刷新间隔</div>
<div class="interval-control">
<input type="number" id="ks-interval" value="${CONFIG.refreshInterval/1000}" min="1" max="300">
<span class="interval-unit">秒</span>
<button class="btn-apply">应用</button>
</div>
<div class="interval-presets">
<button>1秒</button>
<button>5秒</button>
<button>15秒</button>
<button>30秒</button>
</div>
</div>
<div class="log-section">
<div class="log-header">
<span>📋 运行日志</span>
<div>
<span style="cursor:pointer;margin-right:10px">📋复制</span>
<span style="cursor:pointer">🗑️清空</span>
</div>
</div>
<div class="log-content" id="ks-log-content">
<div class="log-entry info">等待操作...</div>
</div>
</div>
</div>
`;
document.body.appendChild(panel);
makeDraggable(panel, panel.querySelector('.panel-header'));
displayInterval = setInterval(updateDisplay, 1000);
updateLogPanel();
}
function updateLogPanel() {
const logContent = document.getElementById('ks-log-content');
if (!logContent) return;
logContent.innerHTML = logHistory.slice(0, 30).map(entry =>
`<div class="log-entry ${entry.type}"><span class="log-time">${entry.time}</span>${entry.message}</div>`
).join('');
}
function updateDisplay() {
const el1 = document.getElementById('ks-monitor-duration');
const el2 = document.getElementById('ks-live-duration');
const el3 = document.getElementById('ks-total-live');
const el4 = document.getElementById('ks-check-count');
if (el1) el1.textContent = formatDuration(getMonitorDuration());
if (el2) el2.textContent = formatDuration(getLiveDuration());
if (el3) el3.textContent = formatDuration(totalLiveDuration + getLiveDuration());
if (el4) el4.textContent = checkCount;
}
function updateStatusIndicator(isLive, isStopped = false) {
const dot = document.getElementById('ks-status-dot');
const text = document.getElementById('ks-status-text');
if (!dot || !text) return;
if (isStopped) {
dot.className = 'status-dot stopped';
text.textContent = '已停止';
} else if (isLive) {
dot.className = 'status-dot live';
text.textContent = '🔴 直播中!';
} else if (isMonitoring) {
dot.className = 'status-dot monitoring';
text.textContent = '监听中...';
} else {
dot.className = 'status-dot stopped';
text.textContent = '未启动';
}
}
function makeDraggable(element, handle) {
let isDragging = false;
let offsetX, offsetY;
handle.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - element.offsetLeft;
offsetY = e.clientY - element.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
element.style.left = (e.clientX - offsetX) + 'px';
element.style.top = (e.clientY - offsetY) + 'px';
element.style.right = 'auto';
}
});
document.addEventListener('mouseup', () => { isDragging = false; });
}
// ==================== 全局函数 ====================
const global = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
global.ksRefreshStart = startMonitor;
global.ksRefreshStop = stopMonitor;
global.ksRefreshReset = resetStats;
global.ksRefreshSetInterval = function(val) {
val = Math.max(1, Math.min(300, parseInt(val) || 30)); // 最小1秒
CONFIG.refreshInterval = val * 1000;
GM_setValue('refreshInterval', CONFIG.refreshInterval);
const input = document.getElementById('ks-interval');
if (input) input.value = val;
log(`⏱️ 刷新间隔已更新: ${val}秒`);
};
global.ksRefreshClearLogs = function() {
logHistory.length = 0;
GM_setValue('logHistory', '[]');
updateLogPanel();
log('🗑️ 日志已清空');
};
global.ksRefreshCopyLogs = function() {
const logText = logHistory.map(entry => `[${entry.time}] ${entry.message}`).join('\n');
navigator.clipboard.writeText(logText).then(() => {
log('✅ 日志已复制到剪贴板');
}).catch(() => {
const textarea = document.createElement('textarea');
textarea.value = logText;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
log('✅ 日志已复制到剪贴板');
});
};
// ==================== 菜单命令 ====================
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand('🔄 打开监听面板', () => { createPanel(); panel.style.display = 'block'; });
GM_registerMenuCommand('▶️ 开始监听', startMonitor);
GM_registerMenuCommand('⏹️ 停止监听', stopMonitor);
}
// ==================== 自动启动 ====================
window.addEventListener('load', () => {
setTimeout(() => {
createPanel();
log('✅ 快手直播监听(刷新版)已加载');
log(`📍 当前页面: ${location.pathname}`);
if (isMonitoring) {
log('🔄 恢复监听状态...');
updateStatusIndicator(isCurrentlyLive);
setTimeout(doCheck, 2000);
} else {
log('💡 点击"开始监听"开始检测');
}
}, 1500);
});
})();