378 lines
15 KiB
JavaScript
378 lines
15 KiB
JavaScript
// ==UserScript==
|
||
// @name 海角社区m3u8提取+去广告-原位替换终极播放版(binghe修改版)
|
||
// @namespace haijiao-analyst
|
||
// @version 4.0
|
||
// @author binghe修改版
|
||
// @description (手机版) 1.自动屏蔽广告 2.捕获m3u8 3.内置HLS引擎+强力播放器节点注入兜底
|
||
// @license MIT
|
||
// @match *://*.haijiao.com/*
|
||
// @match *://*/post/details*
|
||
// @grant GM_addStyle
|
||
// @grant unsafeWindow
|
||
// @run-at document-start
|
||
// ==/UserScript==
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
let capturedM3u8Url = '';
|
||
let hasReplaced = false;
|
||
const btnId = 'hj-play-btn-mobile';
|
||
|
||
// ==========================================
|
||
// 模块一:强力去广告 (静态数据清洗)
|
||
// ==========================================
|
||
const adStyles = `
|
||
[class*="guanggao"], [class*="ads"], [id*="ads"],
|
||
[class*="banner"], [id*="banner"],
|
||
.float-box, .float-window, .couplet,
|
||
.top-ad, .bottom-ad, .sidebar,
|
||
.ad-container, .gg-box,
|
||
iframe:not([src*="m3u8"]):not([src*="player"]):not([id*="play"]),
|
||
.ad-box img, a[href*="bocai"]
|
||
{
|
||
display: none !important;
|
||
visibility: hidden !important;
|
||
height: 0 !important;
|
||
width: 0 !important;
|
||
opacity: 0 !important;
|
||
pointer-events: none !important;
|
||
}
|
||
body { overflow-x: hidden; }
|
||
`;
|
||
GM_addStyle(adStyles);
|
||
|
||
function cleanUpAds() {
|
||
document.querySelectorAll('div[style*="z-index: 999"]').forEach(div => div.remove());
|
||
document.querySelectorAll('div[style*="position: fixed"]').forEach(div => {
|
||
if (div.id !== btnId && div.id !== 'hj-toast-mobile') {
|
||
if (div.querySelector('iframe') || div.querySelector('img')) {
|
||
div.style.display = 'none';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
setInterval(cleanUpAds, 2000);
|
||
|
||
// ==========================================
|
||
// 模块二:数据下行拦截 (拦截真实 M3U8 地址)
|
||
// ==========================================
|
||
GM_addStyle(`
|
||
#${btnId} {
|
||
position: fixed;
|
||
bottom: 80px;
|
||
right: 15px;
|
||
width: 55px;
|
||
height: 55px;
|
||
border-radius: 50%;
|
||
background-color: rgba(30, 60, 114, 0.95);
|
||
color: white;
|
||
border: 2px solid rgba(255,255,255,0.3);
|
||
z-index: 999999;
|
||
font-size: 24px;
|
||
text-align: center;
|
||
line-height: 51px;
|
||
user-select: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
display: none;
|
||
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
|
||
transition: transform 0.2s, background-color 0.3s;
|
||
}
|
||
#${btnId}.show { display: block; }
|
||
#${btnId}.active { transform: scale(0.9); }
|
||
#${btnId}.played { background-color: rgba(46, 139, 87, 0.95); }
|
||
|
||
#hj-toast-mobile {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: rgba(0,0,0,0.8);
|
||
color: #fff;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
z-index: 1000000;
|
||
font-size: 15px;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
`);
|
||
|
||
const getRealVideoSrc = (content, requestUrl) => {
|
||
if (!content) return "";
|
||
try {
|
||
if (content.includes("#EXTM3U")) {
|
||
const baseUrl = requestUrl.substring(0, requestUrl.lastIndexOf('/') + 1);
|
||
const filenameMatch = content.match(/([\w_]+_?)[\d]+\.ts/);
|
||
if (filenameMatch) return baseUrl + filenameMatch[1] + ".m3u8";
|
||
} else {
|
||
const lines = content.split("\n");
|
||
const tsLine = lines.find(line => line.includes('.ts'));
|
||
if (tsLine) {
|
||
let reg = tsLine.match(/([\w_]+_?)[\d]+\.ts/);
|
||
if (reg) return tsLine.replace(reg[0], reg[1] + ".m3u8").trim();
|
||
}
|
||
}
|
||
} catch (e) { }
|
||
return "";
|
||
};
|
||
|
||
const bareDecode = (text) => { try { return JSON.parse(atob(atob(atob(text)))); } catch (e) { return null; } };
|
||
const decodeEncryptString = (text) => {
|
||
let data = text;
|
||
try {
|
||
if (typeof text === 'string') {
|
||
let tmpDataObj = null;
|
||
try { tmpDataObj = JSON.parse(text); } catch (e) { }
|
||
if (tmpDataObj) {
|
||
if (tmpDataObj.data && typeof tmpDataObj.data === 'object') {
|
||
data = tmpDataObj.data;
|
||
} else if (tmpDataObj.data && typeof tmpDataObj.data === 'string') {
|
||
data = bareDecode(tmpDataObj.data);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) { }
|
||
return data;
|
||
};
|
||
|
||
const originalXHR = unsafeWindow.XMLHttpRequest;
|
||
unsafeWindow.XMLHttpRequest = function () {
|
||
const xhr = new originalXHR();
|
||
const originalOpen = xhr.open;
|
||
const originalSend = xhr.send;
|
||
let requestUrl = '';
|
||
xhr.open = function (method, url) {
|
||
requestUrl = url || '';
|
||
return originalOpen.apply(this, arguments);
|
||
};
|
||
xhr.send = function () {
|
||
if (requestUrl.includes("/api/address/") || requestUrl.includes("/api/topic/")) {
|
||
xhr.addEventListener('load', function () {
|
||
setTimeout(() => handleXhrLoad(xhr, requestUrl), 0);
|
||
});
|
||
}
|
||
return originalSend.apply(this, arguments);
|
||
};
|
||
return xhr;
|
||
};
|
||
|
||
async function handleXhrLoad(xhr, url) {
|
||
try {
|
||
if (url.includes("/api/address/")) {
|
||
const videoSrc = getRealVideoSrc(xhr.responseText, url);
|
||
if (videoSrc) updateState(videoSrc);
|
||
}
|
||
else if (/\/api\/topic\/\d+/.test(url)) {
|
||
const data = decodeEncryptString(xhr.responseText);
|
||
if (data?.attachments) {
|
||
for (const element of data.attachments) {
|
||
if (element.category === "video" && element.remoteUrl) {
|
||
fetch(element.remoteUrl)
|
||
.then(res => res.text())
|
||
.then(content => {
|
||
const videoSrc = getRealVideoSrc(content, element.remoteUrl);
|
||
if (videoSrc) updateState(videoSrc);
|
||
}).catch(() => { });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) { }
|
||
}
|
||
|
||
function updateState(src) {
|
||
if (!src || src === capturedM3u8Url) return;
|
||
capturedM3u8Url = src;
|
||
showToast('已捕获完整 M3U8 索引,正在自动替换播放器...');
|
||
// 自动替换播放器,无需手动点击
|
||
if (!hasReplaced) {
|
||
replaceAndPlayVideo();
|
||
}
|
||
}
|
||
|
||
function showToast(msg) {
|
||
let toast = document.getElementById('hj-toast-mobile');
|
||
if (!toast) {
|
||
toast = document.createElement('div');
|
||
toast.id = 'hj-toast-mobile';
|
||
document.body.appendChild(toast);
|
||
}
|
||
toast.textContent = msg;
|
||
toast.style.opacity = '1';
|
||
setTimeout(() => { toast.style.opacity = '0'; }, 3000);
|
||
}
|
||
|
||
// ==========================================
|
||
// 模块三:高级解析节点注入与 M3U8 流媒体接管
|
||
// ==========================================
|
||
function replaceAndPlayVideo() {
|
||
if (hasReplaced) return;
|
||
hasReplaced = true;
|
||
showToast('开始启动数据管线与播放器构筑...');
|
||
|
||
// 【1】暴力清洗阶段:寻址页面现有的全部正在播放的原生媒体,将其静音并断肠式销毁
|
||
document.querySelectorAll('video').forEach(v => {
|
||
v.pause();
|
||
v.removeAttribute('src');
|
||
v.load();
|
||
const potentialParent = v.closest('.video-div') || v.closest('.dplayer') || v.parentElement;
|
||
if (potentialParent) potentialParent.innerHTML = ''; // 清空它原来的播放外壳
|
||
});
|
||
|
||
// 隐藏那些“付费提示”或“只有30秒预览”等脏数据文案
|
||
document.querySelectorAll('.sell_line1, .sell_line2, .preview-title').forEach(el => {
|
||
el.style.display = 'none';
|
||
});
|
||
|
||
// 【2】模糊寻址阶段:采用多维度的探针去锁定可能作为注入地点的容器
|
||
// 选择器兼容:原始特征、ID特征、购买容器特征
|
||
let videoContainers = document.querySelectorAll('.video-div, .dplayer, [id^="video_"], .sell-btn, .post-details');
|
||
|
||
let targetContainer = null;
|
||
|
||
if (videoContainers.length > 0) {
|
||
targetContainer = videoContainers[0]; // 抓住锁定的第一个点位
|
||
console.log("【数据洞察】常规寻址成功");
|
||
} else {
|
||
// 【3】主动兜底机制 (Proactive Fallback):节点异形变幻到无法寻址?
|
||
// 我们直接凭空生造一个 div 插入在最前面!
|
||
console.log("【数据洞察】常规寻址脱靶,触发降级注入策略!");
|
||
showToast('启用兜底注入模式,强制渲染播放器');
|
||
|
||
targetContainer = document.createElement('div');
|
||
targetContainer.style.margin = '15px 0';
|
||
let anchorNode = document.querySelector('body');
|
||
|
||
// 尝试挂载到内容顶部附近
|
||
let contentBody = document.querySelector('.post-content') || document.querySelector('.article-content') || document.body;
|
||
contentBody.insertBefore(targetContainer, contentBody.firstChild);
|
||
}
|
||
|
||
// 初始化容器 (如果是老的Dplayer容器,就把里面的脏DOM连根拔起)
|
||
targetContainer.innerHTML = '';
|
||
const newPlayerId = 'clean-core-player-' + Date.now();
|
||
|
||
// 【提取页面标题】
|
||
let pageTitle = '未知标题';
|
||
const titleSpan = document.querySelector('.header h2 > span');
|
||
if (titleSpan) {
|
||
pageTitle = titleSpan.textContent.trim();
|
||
} else if (document.title) {
|
||
pageTitle = document.title.trim();
|
||
}
|
||
console.log('[Clean Player] 提取到页面标题:', pageTitle);
|
||
|
||
// 挂载全新的展示区(一体化无缝卡片设计)
|
||
targetContainer.innerHTML = `
|
||
<div style="margin: 0 auto; width: 100%; border-radius: 12px; overflow: hidden; box-shadow: 0 12px 32px rgba(0,0,0,0.9); background-color: #000; border: 1px solid #2a2a2a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
|
||
<div style="margin: 0; padding: 14px 18px; background: #18181c; border-bottom: 1px solid #0d0d0f; display: flex; flex-direction: column; gap: 8px;">
|
||
<div style="font-size: 16px; font-weight: 600; color: #ececec; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; letter-spacing: 0.3px;">
|
||
${pageTitle}
|
||
</div>
|
||
<div style="font-size: 12px; color: #10b981; display: flex; align-items: center; font-weight: 500;">
|
||
<span style="display:inline-block; width: 6px; height: 6px; background-color: #10b981; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 8px rgba(16,185,129,0.8);"></span>
|
||
<span style="opacity: 0.9;">Analytics 探针激活 · 广告及播放限制已彻底解除 · M3U8 直连就绪</span>
|
||
</div>
|
||
</div>
|
||
<video
|
||
id="${newPlayerId}"
|
||
controls
|
||
playsinline
|
||
webkit-playsinline
|
||
style="display: block; width: 100%; max-height: 80vh; background-color: #000; margin: 0; padding: 0; border: none; outline: none;">
|
||
</video>
|
||
</div>
|
||
`;
|
||
|
||
const newVideoTag = targetContainer.querySelector(`#${newPlayerId}`);
|
||
const targetM3u8 = capturedM3u8Url;
|
||
|
||
// 【4】M3U8 智能解析路由
|
||
if (newVideoTag.canPlayType('application/vnd.apple.mpegurl')) {
|
||
// 场景 1: 原生硬解 (iOS)
|
||
newVideoTag.src = targetM3u8;
|
||
showToast('iOS 原生硬解就绪,点击播放');
|
||
} else {
|
||
// 场景 2: 软解加载 (Android / PC)
|
||
const GlobalHls = unsafeWindow.Hls || window.Hls;
|
||
|
||
if (GlobalHls && GlobalHls.isSupported()) {
|
||
let hls = new GlobalHls();
|
||
hls.loadSource(targetM3u8);
|
||
hls.attachMedia(newVideoTag);
|
||
hls.on(GlobalHls.Events.MANIFEST_PARSED, function () {
|
||
console.log('HLS 解析完毕,播放器就绪');
|
||
});
|
||
showToast('HLS 引擎解码就绪,点击播放');
|
||
} else {
|
||
showToast('主动下载边缘节点 M3U8 解析器...');
|
||
let script = document.createElement('script');
|
||
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
|
||
script.onload = () => {
|
||
const InjectedHls = unsafeWindow.Hls || window.Hls;
|
||
if (InjectedHls && InjectedHls.isSupported()) {
|
||
let hls = new InjectedHls();
|
||
hls.loadSource(targetM3u8);
|
||
hls.attachMedia(newVideoTag);
|
||
hls.on(InjectedHls.Events.MANIFEST_PARSED, () => {
|
||
console.log('外置 HLS 引擎解析完毕,播放器就绪');
|
||
showToast('外置引擎挂载完毕,点击播放');
|
||
});
|
||
} else {
|
||
showToast('您的设备底层解码器版本不兼容。');
|
||
}
|
||
};
|
||
document.head.appendChild(script);
|
||
}
|
||
}
|
||
|
||
const btn = document.getElementById(btnId);
|
||
if (btn) {
|
||
btn.classList.add('played');
|
||
btn.textContent = '✔️';
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
if (document.getElementById(btnId)) return;
|
||
const btn = document.createElement('div');
|
||
btn.id = btnId;
|
||
btn.textContent = '▶️';
|
||
|
||
btn.onclick = function () {
|
||
btn.classList.add('active');
|
||
setTimeout(() => btn.classList.remove('active'), 100);
|
||
|
||
if (capturedM3u8Url) {
|
||
// 备用手动触发(若自动替换未执行)
|
||
hasReplaced = false;
|
||
replaceAndPlayVideo();
|
||
} else {
|
||
showToast('后台尚未捕获到完整数据的索引参数,请等待刷新或播放原视频探寻。');
|
||
}
|
||
};
|
||
document.body.appendChild(btn);
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
window.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
window.addEventListener('popstate', () => {
|
||
capturedM3u8Url = '';
|
||
hasReplaced = false;
|
||
const btn = document.getElementById(btnId);
|
||
if (btn) {
|
||
btn.classList.remove('show');
|
||
btn.classList.remove('played');
|
||
btn.textContent = '▶️';
|
||
}
|
||
setTimeout(cleanUpAds, 500);
|
||
});
|
||
|
||
})();
|