// ==UserScript==
// @name 海角社区
// @name:zh-TW 海角社區
// @namespace http://tampermonkey.net/
// @version v1.9
// @description 🚀新功能上线,不浪费一秒,会员随时停启💰。付费内容无限次观看、便捷下载和即时分享链接。受益于保存的登录信息、媒体帖子视觉提示(红色标题)、内容自动展开和完全无广告的环境🚀。
// @description:zh-TW 🚀新功能上線,不浪費一秒,會員隨時停啟💰。付費內容無限次觀看、便捷下載和即時分享連結受益於保存的登錄信息、媒體帖子視覺提示(紅色標題)、內容自動展開和完全無廣告的環境🚀。
// @author lx
// @match https://hjcx.org/*
// @match https://hjcx.org/*
// @match https://www.huajitv.com/*
// @match https://huajitv.com/*
// @match https://www.haijiao.com/*
// @match https://haijiao.com/*
// @match https://haijiao.ai/*
// @match */post/details?pid=*
// @exclude *://www.haijiaom.cc/*
// @run-at document-start
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @connect haijiao.live
// @charset UTF-8
// @antifeature payment
// @license MIT
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// ==/UserScript==
(function () {
let baseUrl = "https://www.haijiaom.cc/api/app-api";
// let baseUrl = "http://127.0.0.1:8080/api/app-api";
let vipUser = {};
let isPlaying = false;
let siteCode = "HJSQ";
let sDataId = "";
let slefVideo = {
videoPlayUrl: "",
videoDuration: 0,
id: 0,
articleId: 0,
videoBaseUrl: "",
videoArticleId: "",
downloadUrl: "",
};
let hjsqUserKye = "hjsq_gm_key";
let baseDownadloadUrl = baseUrl + "/business/download/down.m3u8?token=";
let m3u8Url = "";
let hasPrev = false;
waitForPageLoad()
.then((body) => {
fetchUserInfo();
createNavbar(body);
removeAdds();
initHlsPlayer();
})
.catch((error) => {
console.error("Failed to load page:", error);
});
//创建导航栏
function createNavbar(body) {
const fontAwesome = document.createElement("link");
fontAwesome.rel = "stylesheet";
fontAwesome.href =
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css";
document.head.appendChild(fontAwesome);
const navbar = document.createElement("div");
navbar.id = "custom-navbar";
navbar.style.cssText = `
position: fixed;
top: 20%;
right:-20px;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0);
padding: 15px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
transition: all 0.3s ease;
z-index: 99999;
`;
const buttonsContainer = document.createElement("div");
buttonsContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 20px;
`;
// Add event.preventDefault() to button actions
const buttons = [
{
id: "personalInfoBtn",
icon: "fas fa-user",
action: (e) => {
e.preventDefault();
createPersonalInfoDialog();
},
},
{
id: "downloadBtn",
icon: "fas fa-download",
action: (e) => {
e.preventDefault();
createDownloadDialog();
},
},
{
id: "playBtn",
icon: "fas fa-play",
action: (e) => {
e.preventDefault();
togglePlay();
},
},
{
id: "toggleBtn",
icon: "fas fa-eye-slash",
action: (e) => {
e.preventDefault();
toggleVisibility();
},
},
];
const buttonStyle = `
width: 20px;
height: 20px;
border: none;
border-radius: 50%;
background: rgb(0, 0, 0,0.2);
color: white;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
`;
buttons.forEach((btn) => {
const button = document.createElement("button");
button.id = btn.id;
button.innerHTML = `<i class="${btn.icon}"></i>`;
button.style.cssText = buttonStyle;
button.addEventListener("click", btn.action);
buttonsContainer.appendChild(button);
});
navbar.appendChild(buttonsContainer);
body.appendChild(navbar);
// Floating toggle button
const floatingBtn = document.createElement("button");
floatingBtn.id = "floating-toggle-btn";
floatingBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 20px;
height: 20px;
border: none;
border-radius: 50%;
background: #4a90e2;
color: white;
cursor: pointer;
transition: all 0.2s ease;
display: none;
align-items: center;
justify-content: center;
font-size: 12px;
z-index: 9998;
`;
floatingBtn.innerHTML = '<i class="fas fa-eye"></i>';
floatingBtn.addEventListener("click", toggleVisibility);
document.body.appendChild(floatingBtn);
}
// 等待页面加载完成
function createLoginFormView() {
const loginDialog = document.createElement("div");
let existingDialog = document.querySelector('[id^="login-dialog"]');
if (existingDialog) return;
loginDialog.innerHTML = `
<style>
.login-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: 90%;
max-width: 400px;
box-sizing: border-box;
}
.login-input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
.login-btn {
width: 100%;
padding: 12px;
background: #4a90e2;
color: white;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
}
.login-error {
color: #ff4d4f;
font-size: 13px;
margin-top: 8px;
}
@media (max-width: 480px) {
.login-container {
padding: 20px;
width: 70%;
}
h2 {
font-size: 20px !important;
margin-bottom: 8px !important;
}
p {
font-size: 13px hai;
}
.login-input {
padding: 10px;
font-size: 13px;
}
.login-btn {
padding: 10px;
font-size: 14px;
}
.home-link {
text-align: center;
margin-top: 15px;
}
.home-link a {
color: #4a90e2;
text-decoration: none;
font-size: 14px;
}
.home-link a:hover {
text-decoration: underline;
}
}
</style>
<div class="login-container" id="login-dialog">
<div style="text-align: right;">
<span class="dialog-close" style="cursor: pointer; font-size: 18px;">×</span>
</div>
<div style="margin-bottom: 20px;">
<h2 style="margin: 0 0 8px; color: #333; font-size: 22px; font-weight: 600;">欢迎登录</h2>
<p style="margin: 0; color: #666; font-size: 14px;">请输入您的账号密码</p>
</div>
<div style="margin-bottom: 15px;">
<input type="text" id="username" placeholder="用户名" class="login-input">
</div>
<div style="margin-bottom: 20px;">
<input type="password" id="password" placeholder="密码" class="login-input">
</div>
<button id="loginSubmit" class="login-btn">
<span id="loginText">登 录</span>
<span id="loginLoading" style="display:none;">
<i class="fas fa-spinner fa-spin"></i>
</span>
</button>
<div class="home-link">
<a href="https://www.haijiaom.cc" id="goHome">
<i class="fas fa-home"></i> 会员注册
</a>
</div>
<div id="loginError" class="login-error"></div>
</div>
`;
document.body.appendChild(loginDialog);
// Make login dialog responsive
// Close dialog when clicking X
document.querySelector(".dialog-close").addEventListener("click", () => {
loginDialog.remove();
});
// 添加输入框焦点效果
const inputs = loginDialog.querySelectorAll("input");
inputs.forEach((input) => {
input.addEventListener("focus", () => {
input.style.borderColor = "#4a90e2";
});
input.addEventListener("blur", () => {
input.style.borderColor = "#ddd";
});
});
document
.getElementById("loginSubmit")
.addEventListener("click", async () => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const loginBtn = document.getElementById("loginSubmit");
const loginText = document.getElementById("loginText");
const loginLoading = document.getElementById("loginLoading");
loginText.style.display = "none";
loginLoading.style.display = "inline-block";
loginBtn.disabled = true;
if (!username || !password) {
const errorDiv = document.getElementById("loginError");
errorDiv.textContent = "请输入用户名和密码";
errorDiv.style.display = "block";
return;
}
try {
const response = await fetch(baseUrl + "/business/member/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.code == 200 && data.data.access_token) {
GM_setValue("myToken", data.data.access_token);
// localStorage.setItem("token", data.accessToken);
loginDialog.remove();
} else {
throw new Error(data.msg || "登录失败");
}
} catch (error) {
const errorDiv = document.getElementById("loginError");
errorDiv.textContent = error.message;
errorDiv.style.display = "block";
loginText.style.display = "inline-block";
loginLoading.style.display = "none";
loginBtn.disabled = false;
}
});
}
function createLoginForm() {
// 防止重复创建
if (document.getElementById("login-container-unique-v8")) {
return;
}
// 创建主容器
const container = document.createElement("div");
container.id = "login-container-unique-v8";
container.innerHTML = `
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap');
#login-container-unique-v8 {
all: initial !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background: transparent !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
z-index: 2147483647 !important;
font-family: 'Poppins', sans-serif !important;
font-size: 12px !important;
opacity: 0;
transition: opacity 0.5s ease !important;
}
#login-container-unique-v8 * {
font-family: inherit !important;
box-sizing: border-box !important;
}
.login-box-v8 {
background: rgba(75, 75, 214, 0.85) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
border-radius: 20px !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37) !important;
padding: 16px 40px !important;
width: 60% !important;
max-width: 400px !important;
text-align: center !important;
color: white !important;
position: relative !important;
transform: scale(0.9);
transition: transform 0.5s ease !important;
}
.login-title-v8 {
font-size: 16px !important;
font-weight: 600 !important;
margin-bottom: 32px !important;
}
.input-group-v8 {
position: relative !important;
margin-bottom: 24px !important;
}
.login-input-v8 {
width: 100% !important;
padding: 6px 6px 6px 48px !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
border-radius: 10px !important;
font-size: 12px !important;
background: rgba(255, 255, 255, 0.2) !important;
color: white !important;
transition: background-color 0.3s, box-shadow 0.3s !important;
}
.login-input-v8::placeholder {
color: rgba(255, 255, 255, 0.7) !important;
}
.login-input-v8:focus {
outline: none !important;
background: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5) !important;
}
.input-icon-v8 {
position: absolute !important;
left: 16px !important;
top: 50% !important;
transform: translateY(-50%) !important;
color: rgba(255, 255, 255, 0.8) !important;
width: 20px !important;
height: 20px !important;
}
.login-button-v8 {
width: 100% !important;
padding: 6px !important;
border: none !important;
border-radius: 10px !important;
background: #ff6b6b !important;
color: white !important;
font-size: 18px !important;
font-weight: 600 !important;
cursor: pointer !important;
transition: background-color 0.3s, transform 0.2s !important;
margin-bottom: 16px !important; /* 为下方的链接留出空间 */
}
.login-button-v8:hover {
background: #ff4757 !important;
transform: translateY(-2px) !important;
}
.close-button-v8 {
position: absolute !important;
top: 15px !important;
right: 15px !important;
background: none !important;
border: none !important;
font-size: 16px !important;
color: rgba(255, 255, 255, 0.8) !important;
cursor: pointer !important;
transition: color 0.3s, transform 0.3s !important;
line-height: 1 !important;
padding: 0 !important;
}
.close-button-v8:hover {
color: white !important;
transform: rotate(90deg) !important;
}
/* 新增: 注册链接区域样式 */
.extra-links-v8 {
font-size: 12px !important;
color: rgba(255, 255, 255, 0.8) !important;
}
.register-link-v8 {
color: #ff8c8c !important;
text-decoration: none !important;
font-weight: 600 !important;
cursor: pointer !important;
transition: color 0.3s !important;
}
.register-link-v8:hover {
text-decoration: underline !important;
color: #ffffff !important;
}
.error-message-v8 {
color: #ffcdd2 !important;
font-size: 14px !important;
text-align: left !important;
margin-top: -16px !important; /* 放在输入框和按钮之间 */
margin-bottom: 16px !important;
min-height: 21px !important; /* 预留空间防止布局抖动 */
visibility: hidden;
opacity: 0;
transition: opacity 0.3s ease-in-out !important;
}
.error-message-v8.visible {
visibility: visible;
opacity: 1;
}
</style>
<div class="login-box-v8">
<button class="close-button-v8">×</button>
<h2 class="login-title-v8">欢迎回来</h2>
<div class="input-group-v8">
<svg class="input-icon-v8" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2V7a5 5 0 00-5-5zm0 2.5a2.5 2.5 0 012.5 2.5V7h-5V7a2.5 2.5 0 012.5-2.5z"></path></svg>
<input type="text" class="login-input-v8" placeholder="用户名">
</div>
<div class="input-group-v8">
<svg class="input-icon-v8" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg>
<input type="password" class="login-input-v8" placeholder="密码">
</div>
<p class="error-message-v8"></p>
<button class="login-button-v8">登录</button>
<!-- 新增: 注册链接 -->
<div class="extra-links-v8">
<span>没有账户? <a class="register-link-v8">立即注册</a></span>
</div>
</div>
`;
document.body.appendChild(container);
const loginBox = container.querySelector(".login-box-v8");
const errorElement = container.querySelector(".error-message-v8");
requestAnimationFrame(() => {
container.style.opacity = "1";
if (loginBox) {
loginBox.style.transform = "scale(1)";
}
});
const showError = (message) => {
errorElement.textContent = message;
errorElement.classList.add("visible");
};
const hideError = () => {
errorElement.classList.remove("visible");
};
const close = () => {
container.style.opacity = "0";
if (loginBox) {
loginBox.style.transform = "scale(0.9)";
}
setTimeout(() => {
if (document.body.contains(container)) {
document.body.removeChild(container);
}
}, 500);
};
container.querySelector(".close-button-v8").onclick = close;
container.querySelector(".login-button-v8").onclick = async () => {
const username = container.querySelector('input[type="text"]').value;
const password = container.querySelector('input[type="password"]').value;
if (!username || !password) {
showError("请输入用户名和密码!");
return;
}
try {
const response = await fetch(baseUrl + "/business/member/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (data.code == 200 && data.data.access_token) {
GM_setValue("myToken", data.data.access_token);
// localStorage.setItem("token", data.accessToken);
fetchUserInfo();
container.remove();
} else {
throw new Error(data.msg || "登录失败");
}
} catch (error) {
showError("用户名或密码错误。");
}
};
// 新增: 注册链接点击事件
container.querySelector(".register-link-v8").onclick = (e) => {
e.preventDefault();
window.location.href = "https://www.haijiaom.cc";
// 在这里可以添加跳转到注册页或显示注册模态框的逻辑
};
}
function createPersonalInfoDialog() {
if (!checkToken()) {
return;
}
let featureEnabled = GM_getValue("feature_enabled", true);
let memberLvel = vipUser.sites.filter((item) => {
return item.siteCode === siteCode;
});
if (memberLvel[0]) {
featureEnabled = memberLvel[0].scriptOpen;
}
const dialog = document.createElement("div");
dialog.innerHTML = `
<style>
.info-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
width: 90%;
max-width: 320px;
box-sizing: border-box;
z-index: 10000;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.info-close {
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
background: #f5f5f5;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.info-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: #4a90e2;
color: white;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 15px;
}
.info-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin: 15px 0;
}
.info-stat-item {
background: #f8f9fa;
padding: 10px;
border-radius: 8px;
text-align: center;
}
.info-actions {
display: grid;
gap: 10px;
margin-top: 15px;
}
.vip-btn {
background: #4a90e2;
color: white;
padding: 10px;
border-radius: 8px;
text-decoration: none;
text-align: center;
font-size: 14px;
}
.logout-btn {
background: #f44336;
color: white;
border: none;
padding: 10px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.expired-notice {
background: #fff3cd;
color: #856404;
padding: 10px;
border-radius: 8px;
margin: 10px 0;
font-size: 13px;
}
.script-settings {
border-top: 1px solid #f0f0f0;
margin-top: 15px;
padding-top: 10px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.setting-item span {
font-size: 14px;
color: #333;
}
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4a90e2;
}
input:checked + .slider:before {
transform: translateX(20px);
}
</style>
<div class="info-dialog">
<div class="info-header">
<h3 style="margin:0">个人信息</h3>
<button class="info-close">×</button>
</div>
<div class="info-avatar">
<i class="fas fa-user fa-2x"></i>
</div>
<div style="text-align:center">
<h2 style="margin:0;font-size:18px">${
vipUser.username || "未登录"
}</h2>
<p style="margin:5px 0;color:#666;font-size:13px">
${vipUser.vipLevel || "普通用户"}
</p>
</div>
<div class="info-stats">
<div class="info-stat-item">
<div style="font-size:18px;color:#4a90e2;font-weight:bold">
${vipUser.downloadNum || 0}
</div>
<div style="font-size:12px;color:#666">下载次数</div>
</div>
<div class="info-stat-item">
<div style="font-size:18px;color:#4a90e2;font-weight:bold">
${vipUser.watchNum || 0}
</div>
<div style="font-size:12px;color:#666">观看次数</div>
</div>
</div>
${
vipUser.expireTime
? `
<div class="expired-notice">
${
new Date(vipUser.expireTime) > new Date()
? `会员有效期至:${new Date(
vipUser.expireTime
).toLocaleDateString()}`
: "您的会员已过期,请重新开通"
}
</div>
`
: ""
}
<div class="script-settings">
<div class="setting-item">
<span>脚本开关</span>
<label class="switch">
<input type="checkbox" id="featureToggle" ${
featureEnabled ? "checked" : ""
}>
<span class="slider"></span>
</label>
</div>
</div>
<div class="info-actions">
<a href="https://www.haijiaom.cc/" class="vip-btn">
<i class="fas fa-crown"></i>
${
vipUser.expireTime &&
new Date(vipUser.expireTime) > new Date()
? "续费会员"
: "开通会员"
}
</a>
<button class="logout-btn" id="logoutBtn">
<i class="fas fa-sign-out-alt"></i> 退出登录
</button>
</div>
</div>
`;
document.body.appendChild(dialog);
const featureToggle = dialog.querySelector("#featureToggle");
if (featureToggle) {
featureToggle.addEventListener("change", async () => {
const isEnabled = featureToggle.checked;
featureToggle.disabled = true;
try {
const result = await putAsync(
baseUrl + "/business/member/script-operation",
{
scriptOpen: isEnabled,
siteCode: siteCode,
}
);
if (result.code == 200) {
GM_setValue("feature_enabled", isEnabled);
showApiMessage(
`功能已${isEnabled ? "开启" : "关闭"},页面即将刷新...`
);
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showApiMessage(result.msg || "操作失败", "error");
featureToggle.checked = !isEnabled;
}
} catch (error) {
showApiMessage("请求失败,请检查网络连接", "error");
featureToggle.checked = !isEnabled;
} finally {
featureToggle.disabled = false;
}
});
}
// Add logout handler
dialog.querySelector("#logoutBtn").addEventListener("click", () => {
GM_setValue("myToken", "");
dialog.remove();
setTimeout(() => window.location.reload(), 1000);
});
// Add close handler
dialog.querySelector(".info-close").addEventListener("click", () => {
dialog.remove();
});
}
const baseTools = [
{
name: "在线下载1",
icon: "fas fa-bolt",
url: "https://tools.thatwind.com/tool/m3u8downloader",
clients: ["windows", "mac"],
},
{
name: "在线下载2",
icon: "fas fa-cloud-download-alt",
url: "http://tools.bugscaner.com/m3u8.html",
clients: ["windows", "mac", "linux"],
},
];
// 根据客户端类型获取工具列表
function getDownloadTools() {
const client = detectClient();
const mobileTools = [
{
name: "安卓视频下载软件",
icon: "fas fa-mobile-alt",
url: "market://details?id=com.xunlei.downloadprovider",
clients: ["android"],
},
{
name: "苹果视频下载软件",
icon: "fas fa-file-download",
url: "https://apps.apple.com/cn/app/m3u8-mpjex/id6449724938",
clients: ["ios"],
},
];
const tools = [...baseTools, ...mobileTools].filter((tool) =>
tool.clients.includes(client)
);
return tools;
}
// 检测客户端类型
function detectClient() {
const ua = navigator.userAgent.toLowerCase();
if (/android/i.test(ua)) return "android";
if (/iphone|ipad|ipod/i.test(ua)) return "ios";
if (/macintosh/i.test(ua)) return "mac";
if (/linux/i.test(ua)) return "linux";
return "windows";
}
function createDownloadDialog(downloadUrl) {
if (!checkToken()) {
return;
}
const firstDialog = document.createElement("div");
firstDialog.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 25px;
padding-top: 40px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
z-index: 10000;
min-width: 320px;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
animation: fadeIn 0.3s ease;
`;
const styleSheet = document.createElement("style");
styleSheet.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, -48%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
.dialog-btn {
padding: 10px 24px;
margin: 0 8px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.dialog-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.primary-btn {
background: linear-gradient(135deg, #4a90e2, #357abd);
color: white;
}
.secondary-btn {
background: #f5f5f5;
color: #666;
}
.dialog-close {
position: absolute;
top: 10px;
right: 10px;
width: 24px;
height: 24px;
border-radius: 50%;
background: #f5f5f5;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
color: #666;
font-size: 14px;
}
.dialog-close:hover {
background: #e0e0e0;
transform: rotate(90deg);
}
`;
document.head.appendChild(styleSheet);
firstDialog.innerHTML = `
<button class="dialog-close">✕</button>
<h3 style="margin: 0 0 20px; font-size: 20px; color: #333; font-weight: 500;">下载确认</h3>
<p style="margin: 0 0 8px; color: #666; font-size: 15px;">本次下载将消耗1次下载机会</p>
<p style="margin: 0 0 25px; font-size: 15px;">
剩余下载次数:<span style="color: #ff4444; font-weight: 600;">${vipUser.downloadNum}</span>
</p>
<div style="margin-top: 25px;">
<button id="confirmDownload" class="dialog-btn primary-btn">确认下载</button>
<button id="cancelDownload" class="dialog-btn secondary-btn">取消</button>
</div>
`;
document.body.appendChild(firstDialog);
function showDownloadOptions() {
const secondDialog = document.createElement("div");
secondDialog.style.cssText = firstDialog.style.cssText;
const downloadTools = getDownloadTools();
const toolButtons = downloadTools
.map(
(tool) => `
<button id="${tool.name}" class="dialog-btn primary-btn" style="
width: 100%;
margin: 8px 0;
background: linear-gradient(135deg, #4CAF50, #45a049);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<i class="${tool.icon}"></i>${tool.name}
</button>
`
)
.join("");
secondDialog.innerHTML = `
<button class="dialog-close">✕</button>
<h3 style="margin: 0 0 20px; font-size: 20px; color: #333; font-weight: 500;">下载选项</h3>
<div style="margin: 20px 0;">
<button id="copyLink" class="dialog-btn primary-btn" style="
width: 100%;
margin: 8px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
">
<i class="fas fa-copy"></i>复制下载链接
</button>
${toolButtons}
</div>
`;
document.body.appendChild(secondDialog);
// 为每个工具添加点击事件
downloadTools.forEach((tool) => {
document.getElementById(tool.name)?.addEventListener("click", () => {
window.open(tool.url, "_blank");
});
});
// 复制链接事件
document.getElementById("copyLink").addEventListener("click", () => {
navigator.clipboard.writeText(slefVideo.downloadUrl);
showApiMessage("下载链接已复制到剪贴板");
});
// 关闭按钮事件
secondDialog
.querySelector(".dialog-close")
.addEventListener("click", () => {
secondDialog.remove();
});
}
// 确认下载事件
document
.getElementById("confirmDownload")
.addEventListener("click", async () => {
firstDialog.remove();
let result = await gcreateDownloadUrl();
if (result.code == 200) {
slefVideo.downloadUrl = baseDownadloadUrl + result.data;
if (vipUser.downloadNum == 0) {
showApiMessage("下载次数已用完", "error");
return;
}
showDownloadOptions();
}
});
// 取消下载事件
document.getElementById("cancelDownload").addEventListener("click", () => {
firstDialog.remove();
});
// 关闭按钮事件
firstDialog.querySelector(".dialog-close").addEventListener("click", () => {
firstDialog.remove();
});
}
function initHlsPlayer() {
const hlsScript = document.createElement("script");
hlsScript.src =
"https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.8/hls.min.js";
document.head.appendChild(hlsScript);
const playerContainer = document.createElement("div");
playerContainer.id = "hls-player-container";
playerContainer.style.cssText = `
position: fixed;
top: 0;
bottom: 0;
left:0;
right:0;
background: rgba(0, 0, 0, 1);
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 9996;
display: none;
`;
// Add close button for player
const closeButton = document.createElement("button");
closeButton.innerHTML = '<i class="fas fa-times"></i>';
closeButton.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.5);
color: white;
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
font-size:16px;
cursor: pointer;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
`;
closeButton.onclick = () => {
window.hlsPlayer.hide();
};
playerContainer.appendChild(closeButton);
let currentUrl = "";
// Create video element
const video = document.createElement("video");
video.id = "hls-video";
video.style.cssText = `
width: 100%;
height: 100%;
object-fit: contain;
`;
video.controls = true;
playerContainer.appendChild(video);
document.body.appendChild(playerContainer);
hlsScript.onload = () => {
window.hlsPlayer = {
setContent: (input) => {
if (isM3U8Content(input)) {
// Handle M3U8 content string
currentUrl = createBlobUrl(input);
} else {
// Handle URL
currentUrl = input;
}
},
play: () => {
if (!currentUrl) {
console.error("Please set URL first");
return;
}
playerContainer.style.display = "block";
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(currentUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play();
isPlaying = true;
updatePlayButton();
});
}
},
pause: () => {
video.pause();
isPlaying = false;
updatePlayButton();
},
toggle: () => {
if (isPlaying) {
window.hlsPlayer.pause();
} else {
window.hlsPlayer.play();
}
},
hide: () => {
playerContainer.style.display = "none";
video.pause();
isPlaying = false;
updatePlayButton();
toggleVisibility();
},
};
};
}
function isM3U8Content(str) {
return str.trim().startsWith("#EXTM3U");
}
function updatePlayButton() {
const playBtn = document.getElementById("playBtn");
if (playBtn) {
playBtn.innerHTML = isPlaying
? '<i class="fas fa-pause"></i>'
: '<i class="fas fa-play"></i>';
playBtn.style.background = isPlaying ? "#4CAF50" : "#4a90e2";
}
}
async function togglePlay() {
if (!(await checkToken())) {
return;
}
if (!(await checkPermission())) {
return;
}
if (slefVideo.videoPlayUrl == "") {
showApiMessage(
"当前页面没有视频,或者当前视频无需VIP权限,请在原有网站观看",
"error"
);
return;
}
let params = Object.assign(
{},
{
remoteUrl: slefVideo.videoPlayUrl,
videoArticleId: slefVideo.articleId,
videoDuration: slefVideo.videoDuration,
videoId: slefVideo.id,
siteCode: siteCode,
}
);
let result = await postAsync(baseUrl + "/business/video/serialize", params);
slefVideo.iv = result.data.iv;
slefVideo.videoBaseUrl = result.data.videoBaseUrl;
slefVideo.uri = result.data.videoKeyUri;
slefVideo.tsFileName = result.data.videoTsName;
let temp = generateM3U8FromVideo(slefVideo, slefVideo.videoDuration);
window.hlsPlayer.setContent(temp);
window.hlsPlayer.play();
toggleVisibility();
}
function generateM3U8FromVideo(slefVideo, duration) {
// Generate segment duration between 1.0 and 1.25 seconds
let segmentDuration = (Math.random() * 0.25 + 1.0).toFixed(6);
const totalSegments = Math.ceil(duration / segmentDuration);
let m3u8Content = "#EXTM3U" + "\r\n";
m3u8Content += "#EXT-X-VERSION:3" + "\r\n";
m3u8Content += "#EXT-X-TARGETDURATION:11" + "\r\n";
m3u8Content += "#EXT-X-MEDIA-SEQUENCE:0" + "\r\n";
// 添加加密信息
if (slefVideo.uri) {
m3u8Content += `#EXT-X-KEY:METHOD=AES-128,URI="${slefVideo.videoBaseUrl}/${slefVideo.uri}"`;
if (slefVideo.iv) {
m3u8Content += `,IV=0x${slefVideo.iv}`;
}
m3u8Content += "\n";
}
// 生成视频片段
for (let i = 0; i < totalSegments; i++) {
const remainingDuration = duration - i * segmentDuration;
const currentSegmentDuration = Math.min(
segmentDuration,
remainingDuration
);
m3u8Content += `#EXTINF:${currentSegmentDuration},\n`;
m3u8Content += `${slefVideo.videoBaseUrl}/${slefVideo.tsFileName}${i}.ts\n`;
}
m3u8Content += "#EXT-X-ENDLIST";
return m3u8Content;
}
function createBlobUrl(m3u8Content) {
const blob = new Blob([m3u8Content], { type: "application/x-mpegURL" });
return URL.createObjectURL(blob);
}
function serializeM3u8() {
let m3u8Content = "";
fetch(m3u8Url, {
method: "GET",
})
.then((res) => res.text())
.then((data) => {
m3u8Content = data;
let lines = m3u8Content.split("\n");
slefVideo.m3u8Url = lines[lines.length - 2];
});
}
function toggleVisibility() {
const navbar = document.getElementById("custom-navbar");
const floatingBtn = document.getElementById("floating-toggle-btn");
const isVisible = navbar.style.display !== "none";
navbar.style.display = isVisible ? "none" : "block";
floatingBtn.style.display = isVisible ? "flex" : "none";
}
const OriginalXHR = unsafeWindow.XMLHttpRequest || window.XMLHttpRequest;
function CustomXHR() {
const xhr = new OriginalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
let requestURL = "";
let modifiedRequestURL = "";
const urlMappings = {
"videos/v2/getUrl": (url) => {
// Example: Change URL for video requests
return url.replace(
url,
"https://api.qianyuewenhua.xyz/videos/getPreUrl"
);
},
};
function modifyURL(originalURL) {
for (const [pattern, modifier] of Object.entries(urlMappings)) {
if (originalURL.includes(pattern)) {
modifiedRequestURL = modifier(originalURL);
return modifiedRequestURL;
}
}
return originalURL;
}
// Add URL modifications before open() is called
xhr.open = function () {
requestURL = arguments[1];
let modifiedURL = modifyURL(requestURL);
arguments[1] = modifiedURL;
return originalOpen.apply(this, arguments);
};
// xhr.open = function () {
// requestURL = arguments[1];
// return originalOpen.apply(this, arguments);
// };
xhr.send = function () {
const originalOnReadyStateChange = xhr.onreadystatechange;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
// URL pattern matching
Object.keys(urlPatterns).forEach((pattern) => {
if (new RegExp(pattern).test(requestURL)) {
let response = JSON.parse(xhr.responseText);
const modifiedResponse = urlPatterns[pattern](response);
if (modifiedResponse) {
Object.defineProperty(xhr, "response", {
value: JSON.stringify(modifiedResponse),
});
Object.defineProperty(xhr, "responseText", {
value: JSON.stringify(modifiedResponse),
});
}
console.log(
"[Modified Response]:",
requestURL,
modifiedResponse
);
}
});
} catch (error) {
console.error("[XHR Error]:", error, requestURL);
}
}
}
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(this, arguments);
}
};
return originalSend.apply(this, arguments);
};
return xhr;
}
if (GM_getValue("feature_enabled", true)) {
// Replace XMLHttpRequest
if (typeof unsafeWindow !== "undefined") {
unsafeWindow.XMLHttpRequest = CustomXHR;
} else {
window.XMLHttpRequest = CustomXHR;
}
}
const urlPatterns = {
"api\\/video\\/checkVideoCanPlay(\\?[^/]*)?$": (response) => {
let decodeData = response.data;
let jsonStr = decodeURIComponent(escape(atob(atob(atob(response.data)))));
let objectData = JSON.parse(jsonStr, `utf-8`);
const freeVideo = { type: 1, amount: 0, money_type: 0, vip: 0 };
response.data = Object.assign({}, objectData, freeVideo);
return response;
},
"api\\/banner\\/banner_list(\\?[^/]*)?$": (response) => {
return null;
},
"videos\\/getInfo(\\?[^/]*)?$": (response) => {
if (checkToken() && hasPrev) {
response.data.canPlay = true;
slefVideo.id = response.data.info.id;
}
return response;
},
"videos\\/v2\\/getUrl(\\?[^/]*)?$": (response) => {
if (checkToken() && hasPrev) {
response.data.url = response.data.url.replace(
/start=\d+\&end=\d+\&/,
""
);
m3u8Url = response.data.url;
serializeM3u8();
}
return response;
},
"api\\/favorite\\/v2\\/folderList(\\?[^/]*)?$": (response) => {
return null;
},
"api\\/topic\\/\\d+": (response) => {
let decodeData = response.data;
analysisVideo(decodeData);
return response;
},
"api\\/topic\\/hot\\/topics(\\?.*)?$": (response) => {
let decodeData = response.data;
let refStr = analysisTitle(decodeData);
response.data = btoaUnicode(refStr);
return response;
},
"api\\/login\\/signin": (response) => {
if (response.success) {
let username = $(".login-form input").first().val();
let password = $(".login-form input").eq(1).val();
GM_setValue(hjsqUserKye, JSON.stringify({ username, password }));
}
return response;
},
};
function btoaUnicode(str) {
return btoa(
btoa(
btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode(parseInt(p1, 16));
})
)
)
);
}
// 你想要填充的用户名和密码
let username = "";
let password = "";
// 目标选择器
const formSelector = ".login-form"; // 根据实际情况修改
const inputSelector = "input";
// 当找到输入框时进行填充
function fillInputsIfAvailable(mutationsList, observer) {
// 检查是否找到了目标表单
const form = document.querySelector(formSelector);
if (form) {
// 获取所有的 input 元素
let user = JSON.parse(GM_getValue(hjsqUserKye));
if (user) {
username = user.username || username;
password = user.password || password;
}
console.log(user);
console.log(user.username);
const inputs = form.querySelectorAll(inputSelector);
if (inputs.length >= 2) {
// 停止观察,因为我们已经找到了需要的元素
observer.disconnect();
// 填充用户名和密码
inputs[0].value = username;
inputs[1].value = password;
// 触发事件,确保页面上的任何事件监听器都能捕获到值的变化
["input", "change"].forEach((eventType) => {
inputs[0].dispatchEvent(new Event(eventType));
inputs[1].dispatchEvent(new Event(eventType));
});
}
}
}
// 创建一个 MutationObserver 实例,传递回调函数
const observer = new MutationObserver(fillInputsIfAvailable);
// 配置观察选项:
const config = { attributes: false, childList: true, subtree: true };
// 选择需要观察的节点(在这里是整个 body,但你可以根据情况调整)
const targetNode = document.body;
if (GM_getValue("feature_enabled", true)) {
// 开始观察目标节点
observer.observe(targetNode, config);
// 初始检查一次,以防元素已经存在
fillInputsIfAvailable([], observer);
}
async function analysisVideo(content) {
let jsonStr = decodeURIComponent(escape(atob(atob(atob(content)))));
let objectData = JSON.parse(jsonStr, `utf-8`);
slefVideo.articleId = objectData.topicId;
objectData.attachments.forEach((item) => {
if (item.category == "video" && item.remoteUr != "") {
slefVideo.videoPlayUrl = item.remoteUrl;
slefVideo.m3u8Url = item.remoteUrl;
slefVideo.videoDuration = item.video_time_length;
slefVideo.id = item.id;
// Extract URI from preview URL
}
// if(item.category == "video" && item.remoteUr ==""){
// console.log("普通视频!")
//}
});
}
function analysisTitle(content) {
let jsonStr = decodeURIComponent(escape(atob(atob(atob(content)))));
let objectData = JSON.parse(jsonStr, `utf-8`);
objectData.results.forEach((item) => {
if (item.attachments) {
let dataId = "";
item.attachments.forEach((e) => {
if (item.hasVideo && dataId) {
sDataId = dataId;
markTitle();
}
dataId = e.id;
});
}
});
return JSON.stringify(objectData);
}
function showApiMessage(message = "操作成功", type = "success") {
// 防止重复弹窗
if (document.getElementById("api-message-dialog")) return;
const colorMap = {
success: "#43a047",
error: "#d32f2f",
info: "#1976d2",
warning: "#ffa000",
};
const iconMap = {
success: "fa-check-circle",
error: "fa-exclamation-triangle",
info: "fa-info-circle",
warning: "fa-exclamation-circle",
};
const dialog = document.createElement("div");
dialog.id = "api-message-dialog";
dialog.innerHTML = `
<style>
.api-message-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
color: ${colorMap[type] || "#333"};
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
padding: 24px 20px 16px 20px;
min-width: 220px;
z-index: 10001;
text-align: center;
font-size: 15px;
}
.api-message-close {
margin-top: 18px;
background: ${colorMap[type] || "#333"};
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 24px;
font-size: 14px;
cursor: pointer;
}
</style>
<div class="api-message-dialog">
<div style="font-size:18px;margin-bottom:10px;">
<i class="fas ${iconMap[type] || "fa-info-circle"}"></i>
</div>
<div>${message}</div>
<button class="api-message-close">关闭</button>
</div>
`;
document.body.appendChild(dialog);
dialog.querySelector(".api-message-close").onclick = () => dialog.remove();
setTimeout(() => dialog.remove(), 4500);
}
function waitForPageLoad() {
return new Promise((resolve, reject) => {
// Set timeout for safety
const timeout = setTimeout(() => {
reject("Page load timeout");
}, 10000);
// Check if page is already loaded
if (document.readyState === "complete") {
clearTimeout(timeout);
resolve(document.body);
return;
}
// Create observer to monitor DOM changes
const observer = new MutationObserver((mutations, obs) => {
if (document.readyState === "complete" && document.body) {
clearTimeout(timeout);
obs.disconnect();
resolve(document.body);
}
});
// Start observing
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
});
}
function checkToken() {
const token = GM_getValue("myToken", "");
if (!token) {
createLoginForm();
return false;
}
return true;
}
async function fetchUserInfo() {
try {
const response = await fetch(
baseUrl + "/business/member/info?siteCode=" + siteCode,
{
method: "GET",
headers: {
Authorization: `Bearer ${GM_getValue("myToken", "")}`,
},
}
);
const data = await response.json();
vipUser = data.data;
if (data.code == 401) {
createLoginForm();
// createLoginView();
} else {
checkPermission();
}
return data.data;
} catch (error) {
console.error("Error fetching user info:", error);
throw error;
}
}
async function postAsync(url, body) {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${GM_getValue("myToken", "")}`, // Add token from localStorage
},
body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
if (result.code != 200) {
showApiMessage(result.msg, "error");
}
if (result.code == 401) {
GM_setValue("myToken", "");
createLoginForm();
}
return result;
} catch (error) {
showApiMessage("操作失败", "error");
throw error;
}
}
async function putAsync(url, body) {
try {
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${GM_getValue("myToken", "")}`,
},
body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
if (result.code != 200) {
showApiMessage(result.msg, "error");
}
if (result.code == 401) {
GM_setValue("myToken", "");
createLoginForm();
}
return result;
} catch (error) {
showApiMessage("操作失败", "error");
throw error;
}
}
async function getAsync(url, params) {
try {
// Build query string from params if they exist
const queryString = params
? "?" + new URLSearchParams(params).toString()
: "";
const response = await fetch(baseUrl + url + queryString, {
method: "GET",
headers: {
Authorization: `Bearer ${GM_getValue("myToken", "")}`,
},
});
const result = await response.json();
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
if (result.code != 200 && result.code != 401) {
showApiMessage(result.msg, "error");
}
if (result.code == 401) {
GM_setValue("myToken", "");
createLoginForm();
}
return result;
} catch (error) {
showApiMessage("操作失败", "error");
throw error;
}
}
async function checkPermission() {
var result = await getAsync("/business/member/has-permission", {
siteCode: siteCode,
});
if (result.code == 200) {
hasPrev = true;
return true;
} else {
return false;
}
}
async function serializeVideo() {
let params = Object.assign(
{},
{
videoId: slefVideo.id,
m3u8Url: slefVideo.m3u8Url,
siteCode: siteCode,
}
);
let result = await postAsync(baseUrl + "/business/video/serialize", params);
}
async function gcreateDownloadUrl() {
if (slefVideo.m3u8Url) {
let params = Object.assign(
{},
{
videoId: slefVideo.id,
m3u8Url: slefVideo.m3u8Url,
siteCode: siteCode,
}
);
return await postAsync(
baseUrl + "/business/download/generate-token",
params
);
} else {
showApiMessage("请等待视频加载完成");
}
}
function markTitle() {
waitForElement("img[data-id='" + sDataId + "']").then((element) => {
if (element) {
// 2. 获取目标元素的父元素
const parent = element.parentElement;
// 3. 获取父元素的兄弟元素(即 parent 的父元素下的所有子元素)
const parentSiblings = Array.from(parent.parentElement.children);
// 4. 过滤出 class 包含 "title" 的兄弟元素
const titleElements = parentSiblings.filter((sibling) => {
// 排除自己(parent)
return sibling !== parent && sibling.classList.contains("t-title");
});
const titlePcElements = parentSiblings.filter((sibling) => {
// 排除自己(parent)
return sibling !== parent && sibling.tagName === "H4";
});
titleElements.forEach((element) => {
element.style.color = "red";
});
titlePcElements.forEach((h4Element) => {
const childElements = h4Element.children;
for (let child of childElements) {
child.style.color = "red";
}
});
// 5. 输出结果
}
});
}
function waitForElement(selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver((mutations, obs) => {
if (document.querySelector(selector)) {
obs.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}
function removeAdds() {
GM_addStyle(
//海角
".containeradvertising { display: none; } .my-swipe{ display: none;} .bannerliststyle{display: none;} .topbanmer{display: none;} .btnbox{display: none;} .addbox{display: none;} .van-swipe__track{display: none;} .html-bottom-box{display: none;} #bbs_float_menu{display: none;} #tidio-chat{display: none;} .ishide {max-height: unset !important; overflow: visible !important;position: relative;}"
);
waitForElement(".my-swipe").then((e) => {
e.style.display = "none";
});
waitForElement(".addbox").then((e) => {
e.style.display = "none";
});
}
})();