// ==UserScript== // @name 看看新闻直播一键播放版 // @namespace http://tampermonkey.net/ // @author https://github.com/WannaFlysyx // @version 3.0 // @description 收看看看新闻直播,支持回看当天已播节目和进度条拖动 // @match *://*.kankanews.com/* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; /* ========== RSA 公钥参数(保持不变) ========== */ const N_HEX = 'cfe61ccf516e5115e136c414f5111077847648568b67fea6ad5a181cd5e6687f4f6a2a312514de8d99ae3ad590301a95f869ecca3fc01d8785898f8bb63b9e310970edc33291a993b6a0d664b8d985d956bc90b82211000073161cf0981337eb9040da6c7a9e27fe8d6c02b4c9a28648175ec4b52a928170dc27bc838f9adcef'; const PUB_N = BigInt('0x' + N_HEX); const PUB_E = 65537n; /* ========== RSA 解密(保持不变) ========== */ function modPow(base, exp, mod) { let result = 1n; base = base % mod; while (exp > 0n) { if (exp & 1n) result = (result * base) % mod; exp >>= 1n; base = (base * base) % mod; } return result; } function bytesToBigInt(bytes) { let v = 0n; for (const b of bytes) v = (v << 8n) | BigInt(b); return v; } function bigIntToBytes(v, len) { const out = new Uint8Array(len); for (let i = len - 1; i >= 0; i--) { out[i] = Number(v & 0xffn); v >>= 8n; } return out; } function unpadPKCS1(bytes) { let i = 0; while (i < bytes.length && bytes[i] === 0) i++; if (i < bytes.length && (bytes[i] === 1 || bytes[i] === 2)) { const sep = bytes.indexOf(0, i + 1); if (sep > 0) return bytes.slice(sep + 1); } while (bytes.length > 0 && bytes[0] === 0) bytes = bytes.slice(1); return bytes; } function decryptLiveAddress(t) { if (!t || typeof t !== 'string') return ''; t = t.replace(/\\\//g, '/').replace(/\\/g, '').trim(); const binary = atob(t); let hex = ''; for (let i = 0; i < binary.length; i++) hex += ('0' + binary.charCodeAt(i).toString(16)).slice(-2); hex = hex.toUpperCase(); const chunks = []; for (let i = 0; i < hex.length; i += 256) { const chunkHex = hex.slice(i, i + 256); const chunkBytes = new Uint8Array(chunkHex.length / 2); for (let j = 0; j < chunkBytes.length; j++) chunkBytes[j] = parseInt(chunkHex.slice(j * 2, j * 2 + 2), 16); const c = bytesToBigInt(chunkBytes); const m = modPow(c, PUB_E, PUB_N); const padded = bigIntToBytes(m, 128); chunks.push(unpadPKCS1(padded)); } const total = chunks.reduce((s, c) => s + c.length, 0); const result = new Uint8Array(total); let off = 0; for (const c of chunks) { result.set(c, off); off += c.length; } return new TextDecoder().decode(result); } /* ========== 注入页面脚本(保持不变) ========== */ function injectInterceptor() { const code = ` (function() { function isUrlMatch(url, keyword) { try { return new URL(url, location.href).pathname.includes(keyword); } catch(e) { return String(url).includes(keyword); } } function processResponse(url, text) { if (typeof text !== 'string') return; try { const j = JSON.parse(text); if (!j || j.code !== '1000' || !j.result) return; if (isUrlMatch(url, '/channel/detail')) { const liveAddress = j.result.live_address; if (liveAddress && typeof liveAddress === 'string' && liveAddress.length > 0) { window.postMessage({ source: 'kklive-interceptor', type: 'channel_info', channel_id: j.result.id, channel_name: j.result.name, live_address: liveAddress }, '*'); } } if (isUrlMatch(url, '/programs')) { const programs = j.result.programs; if (Array.isArray(programs) && programs.length > 0) { window.postMessage({ source: 'kklive-interceptor', type: 'program_list', channel_id: j.result.id, channel_name: j.result.name, date: j.result.date || '', programs: programs }, '*'); } } } catch(e) {} } const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._kklive_url = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { if (this._kklive_url) { this.addEventListener('load', function() { processResponse(this._kklive_url, this.responseText); }); } return origSend.apply(this, arguments); }; const origFetch = window.fetch; window.fetch = function(url, opts) { return origFetch.apply(this, arguments).then(function(resp) { const u = (typeof url === 'string') ? url : (url && url.url); if (u) { resp.clone().text().then(raw => processResponse(u, raw)); } return resp; }); }; })(); `; const s = document.createElement('script'); s.textContent = code; (document.head || document.documentElement).appendChild(s); s.remove(); } injectInterceptor(); /* ========== 内容脚本状态(保持不变) ========== */ let capturedLive = null; let decryptedLiveUrl = null; let capturedName = ''; let currentChannelId = null; let programList = []; let currentDate = ''; function getTodayStr() { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return y + '-' + m + '-' + day; } window.addEventListener('message', (e) => { const data = e.data; if (!data || data.source !== 'kklive-interceptor') return; if (data.type === 'channel_info') { capturedLive = data.live_address; capturedName = data.channel_name || '直播'; currentChannelId = data.channel_id; try { decryptedLiveUrl = decryptLiveAddress(capturedLive); console.log('[KKLive] 直播地址解密成功:', decryptedLiveUrl); } catch (err) { console.error('[KKLive] 直播地址解密失败:', err); decryptedLiveUrl = null; } updateButtonState(); updateChannelNameUI(); } if (data.type === 'program_list') { programList = data.programs || []; currentDate = data.date || getTodayStr(); if (data.channel_name) capturedName = data.channel_name; console.log('[KKLive] 捕获到节目列表,共', programList.length, '个节目,日期:', currentDate); updateProgramListUI(); updateDateUI(); } }); /* ========== 加载 hls.js(保持不变) ========== */ function loadHls() { return new Promise((resolve, reject) => { if (window.Hls) return resolve(window.Hls); const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.13/dist/hls.min.js'; s.onload = () => resolve(window.Hls); s.onerror = reject; document.head.appendChild(s); }); } /* ========== 🎨 美化的 UI 面板 ========== */ let panel = null; let floatBtn = null; function createPanel() { if (panel) return panel; panel = document.createElement('div'); panel.id = 'kklive-panel'; panel.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 999999; width: 400px; max-height: 86vh; background: rgba(20, 22, 36, 0.88); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); color: #f0f0f5; border-radius: 20px; box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.06); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; display: flex; flex-direction: column; overflow: hidden; transition: transform 0.25s ease, opacity 0.25s ease; `; // 头部 const header = document.createElement('div'); header.style.cssText = ` padding: 14px 20px; background: linear-gradient(145deg, rgba(255,255,255,0.04), rgba(255,255,255,0.01)); display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.06); `; header.innerHTML = ` 📺 看看播放器 `; panel.appendChild(header); // 频道信息行 const channelRow = document.createElement('div'); channelRow.style.cssText = ` padding: 8px 20px 6px 20px; background: rgba(255,255,255,0.02); display: flex; align-items: center; gap: 6px; font-size: 13px; border-bottom: 1px solid rgba(255,255,255,0.04); `; channelRow.innerHTML = ` 📡 `; panel.appendChild(channelRow); // 直播按钮 const liveBtnWrap = document.createElement('div'); liveBtnWrap.style.cssText = `padding: 10px 20px; border-bottom: 1px solid rgba(255,255,255,0.04);`; const liveBtn = document.createElement('button'); liveBtn.id = 'kklive-live-btn'; liveBtn.textContent = '▶ 播放直播'; liveBtn.style.cssText = ` width: 100%; padding: 10px 0; background: linear-gradient(135deg, #34d399, #22d3ee); color: #0c0e1a; border: none; border-radius: 40px; font-weight: 700; font-size: 14px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 14px rgba(52, 211, 153, 0.3); letter-spacing: 0.3px; `; liveBtn.onmouseenter = () => { liveBtn.style.transform = 'scale(1.02)'; liveBtn.style.boxShadow = '0 6px 20px rgba(52, 211, 153, 0.4)'; }; liveBtn.onmouseleave = () => { liveBtn.style.transform = 'scale(1)'; liveBtn.style.boxShadow = '0 4px 14px rgba(52, 211, 153, 0.3)'; }; liveBtnWrap.appendChild(liveBtn); panel.appendChild(liveBtnWrap); // 节目单头部(日期 + 刷新) const progHeader = document.createElement('div'); progHeader.style.cssText = ` display: flex; align-items: center; justify-content: space-between; padding: 6px 20px 4px 20px; background: rgba(255,255,255,0.02); border-bottom: 1px solid rgba(255,255,255,0.04); `; progHeader.innerHTML = ` `; panel.appendChild(progHeader); // 节目列表容器(可滚动) const listWrap = document.createElement('div'); listWrap.style.cssText = ` flex: 1; overflow-y: auto; padding: 6px 12px 10px 12px; background: rgba(0,0,0,0.15); max-height: 360px; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.15) transparent; `; // 自定义滚动条(webkit) const styleScroll = document.createElement('style'); styleScroll.textContent = ` #kklive-panel ::-webkit-scrollbar { width: 4px; } #kklive-panel ::-webkit-scrollbar-track { background: transparent; } #kklive-panel ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; } #kklive-panel ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.35); } `; panel.appendChild(styleScroll); const listEl = document.createElement('div'); listEl.id = 'kklive-program-list'; listEl.style.cssText = 'display: flex; flex-direction: column; gap: 4px;'; listWrap.appendChild(listEl); panel.appendChild(listWrap); // 视频播放器 const video = document.createElement('video'); video.id = 'kklive-video'; video.style.cssText = ` width: 100%; display: none; background: #000; max-height: 200px; border-top: 1px solid rgba(255,255,255,0.06); `; video.controls = true; video.playsInline = true; panel.appendChild(video); // 状态栏 const status = document.createElement('div'); status.id = 'kklive-status'; status.style.cssText = ` padding: 10px 20px; font-size: 12px; opacity: 0.8; min-height: 22px; background: rgba(0,0,0,0.2); border-top: 1px solid rgba(255,255,255,0.04); color: #c0c0d0; letter-spacing: 0.2px; `; panel.appendChild(status); document.body.appendChild(panel); // 事件绑定 panel.querySelector('#kklive-close').onclick = () => { const videoEl = panel.querySelector('#kklive-video'); if (videoEl._hls) videoEl._hls.destroy(); panel.remove(); panel = null; }; panel.querySelector('#kklive-live-btn').onclick = () => playLive(); panel.querySelector('#kklive-refresh-btn').onclick = () => { setStatus('💡 节目单由页面自动加载,可尝试刷新页面'); if (programList.length === 0) { setStatus('⚠️ 未捕获到节目列表,请刷新页面后重试'); } else { updateProgramListUI(); setStatus('✅ 节目单已更新'); } }; updateChannelNameUI(); updateDateUI(); updateProgramListUI(); return panel; } /* ========== UI 更新辅助函数 ========== */ function updateChannelNameUI() { if (!panel) return; const nameEl = panel.querySelector('#kklive-channel-name'); if (nameEl) nameEl.textContent = capturedName || '未知频道'; } function updateDateUI() { if (!panel) return; const dateEl = panel.querySelector('#kklive-date'); if (dateEl) dateEl.textContent = currentDate ? '📅 ' + currentDate : getTodayStr(); } function setStatus(msg) { if (!panel) return; const statusEl = panel.querySelector('#kklive-status'); if (statusEl) statusEl.textContent = msg; } function updateButtonState() { if (!floatBtn) return; if (decryptedLiveUrl || capturedLive) { floatBtn.textContent = '▶ 直播就绪'; floatBtn.style.background = 'linear-gradient(135deg, #34d399, #22d3ee)'; floatBtn.style.color = '#0c0e1a'; floatBtn.style.boxShadow = '0 6px 24px rgba(52, 211, 153, 0.4)'; } else { floatBtn.textContent = '⏳ 加载中...'; floatBtn.style.background = 'rgba(255,255,255,0.08)'; floatBtn.style.color = '#aaa'; floatBtn.style.boxShadow = 'none'; } } function updateProgramListUI() { if (!panel) return; const listEl = panel.querySelector('#kklive-program-list'); if (!listEl) return; listEl.innerHTML = ''; if (programList.length === 0) { const empty = document.createElement('div'); empty.style.cssText = ` padding: 24px 12px; text-align: center; opacity: 0.4; font-size: 13px; letter-spacing: 0.3px; `; empty.textContent = '📭 暂无节目数据,请等待页面加载'; listEl.appendChild(empty); return; } programList.forEach(prog => { const btn = document.createElement('button'); const start = prog.start_time_string ? prog.start_time_string.slice(11, 16) : ''; const end = prog.end_time_string ? prog.end_time_string.slice(11, 16) : ''; btn.textContent = `${prog.name} ${start} - ${end}`; btn.style.cssText = ` width: 100%; padding: 8px 12px; background: rgba(255,255,255,0.04); color: #e8e8f0; border: none; border-radius: 12px; cursor: pointer; text-align: left; font-size: 12.5px; transition: all 0.15s ease; font-weight: 450; letter-spacing: 0.1px; `; btn.onmouseenter = () => { btn.style.background = 'rgba(255,255,255,0.10)'; btn.style.transform = 'translateX(2px)'; }; btn.onmouseleave = () => { btn.style.background = 'rgba(255,255,255,0.04)'; btn.style.transform = 'translateX(0)'; }; btn.onclick = () => playReplay(prog); listEl.appendChild(btn); }); } /* ========== 浮动按钮(美化) ========== */ function initFloatBtn() { if (floatBtn) return; floatBtn = document.createElement('button'); floatBtn.textContent = '▶ 直播'; floatBtn.style.cssText = ` position: fixed; bottom: 28px; right: 28px; z-index: 999998; padding: 12px 24px; background: linear-gradient(135deg, #34d399, #22d3ee); color: #0c0e1a; border: none; border-radius: 40px; font-size: 14px; font-weight: 700; cursor: pointer; box-shadow: 0 8px 28px rgba(52, 211, 153, 0.35); transition: all 0.25s ease; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; letter-spacing: 0.3px; backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.08); `; floatBtn.onmouseenter = () => { floatBtn.style.transform = 'scale(1.06)'; floatBtn.style.boxShadow = '0 12px 36px rgba(52, 211, 153, 0.5)'; }; floatBtn.onmouseleave = () => { floatBtn.style.transform = 'scale(1)'; floatBtn.style.boxShadow = '0 8px 28px rgba(52, 211, 153, 0.35)'; }; floatBtn.onclick = () => { createPanel(); if (decryptedLiveUrl) { playLive(); } else { setStatus('⏳ 等待直播地址,请刷新页面或稍后重试'); } }; document.body.appendChild(floatBtn); updateButtonState(); } /* ========== 播放功能(保持不变) ========== */ function playLive() { createPanel(); if (!decryptedLiveUrl) { setStatus('❌ 直播地址未就绪,请刷新页面重试'); return; } doPlay(decryptedLiveUrl, capturedName || '直播'); } function playReplay(program) { createPanel(); if (!decryptedLiveUrl) { setStatus('❌ 未获取到频道直播地址,无法回放'); return; } let url = decryptedLiveUrl; if (/\.m3u8/.test(url) && program.start_time && program.end_time) { const sep = url.includes('?') ? '&' : '?'; url += `${sep}start=${program.start_time}&end=${program.end_time}`; } const title = program.name || '回放'; setStatus('⏳ 加载回放:' + title); doPlay(url, title); } async function doPlay(input, name) { createPanel(); setStatus('正在处理地址...'); let url = input; if (!/^https?:\/\//i.test(url)) { setStatus('🔐 解密中...'); try { url = decryptLiveAddress(url); } catch (e) { setStatus('❌ 解密失败:' + e.message); return; } if (!url || !url.startsWith('http')) { setStatus('❌ 解密结果异常'); return; } setStatus('✅ 解密成功,加载播放器...'); } else { setStatus('✅ 加载播放器...'); } const video = panel.querySelector('#kklive-video'); try { const Hls = await loadHls(); video.style.display = 'block'; if (Hls.isSupported()) { if (video._hls) video._hls.destroy(); const hls = new Hls({ liveDurationInfinity: true }); video._hls = hls; hls.loadSource(url); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play().catch(() => {}); setStatus('🎉 正在播放:' + (name || '直播')); }); hls.on(Hls.Events.ERROR, (_, data) => { if (data.fatal) { setStatus('⚠️ 播放错误:' + data.type + ' / ' + data.details); } }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; video.play().catch(() => {}); setStatus('🎉 正在播放(原生HLS):' + (name || '直播')); } else { setStatus('❌ 当前浏览器不支持 HLS'); } } catch (e) { setStatus('❌ 播放器加载失败:' + e.message); } } /* ========== 初始化 ========== */ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFloatBtn); } else { initFloatBtn(); } })();