// ==UserScript==
// @name 小鹅通直播回放链接解密成功提示
// @namespace http://tampermonkey.net/
// @version 1.8
// @description 成功获取m3u8地址后,并提示用户点击右侧按钮复制m3u8地址
// @author 破坏游戏的孩子
// @match https://*.h5.xiaoeknow.com/v3/course/alive/*
// @match https://*.xet.citv.cn/v3/course/alive/*
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_addElement
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js
// @license 未经许可,禁止修改
// ==/UserScript==
(function() {
'use strict';
// 获取当前URL参数
const urlParams = new URLSearchParams(window.location.search);
const appId = urlParams.get('app_id');
const aliveId = window.location.pathname.split('/').pop();
const hostname = window.location.hostname;
if (appId && aliveId) {
// 获取视频详情的接口地址
let baseInfoUrl;
let apiUrl;
if (hostname.includes('xet.citv.cn')) {
baseInfoUrl = `https://${appId}.xet.citv.cn/_alive/v3/base_info?resource_id=${aliveId}&product_id=&type=12&is_direct=1`;
apiUrl = `https://${appId}.xet.citv.cn/_alive/v3/get_lookback_list?app_id=${appId}&alive_id=${aliveId}`;
} else {
baseInfoUrl = `https://${appId}.h5.xiaoeknow.com/_alive/v3/base_info?resource_id=${aliveId}&product_id=&type=12&is_direct=1`;
apiUrl = `https://${appId}.h5.xiaoeknow.com/_alive/v3/get_lookback_list?app_id=${appId}&alive_id=${aliveId}`;
}
// 先获取视频详情
GM_xmlhttpRequest({
method: "GET",
url: baseInfoUrl,
onload: function(baseResponse) {
try {
const baseData = JSON.parse(baseResponse.responseText);
const videoTitle = baseData.data.alive_info.title || '未知标题';
const rawExpireTime = baseData.data.alive_conf.lookback_time.expire || '';
// 格式化时间和倒计时信息
const expireTime = rawExpireTime ? new Date(rawExpireTime * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-') : '长期有效';
// 计算剩余时间
let timeLeftStr = '';
let expireDate = 0;
if (rawExpireTime) {
const now = new Date().getTime();
expireDate = new Date(rawExpireTime * 1000).getTime();
const timeLeft = expireDate - now;
const daysLeft = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
const hoursLeft = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutesLeft = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
const secondsLeft = Math.floor((timeLeft % (1000 * 60)) / 1000);
timeLeftStr = `${daysLeft}天${hoursLeft}小时${minutesLeft}分钟${secondsLeft}秒`;
}
// 继续获取回放列表
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
// 获取所有回放数据
const videoList = data.data || [];
if (videoList.length === 0) {
console.error('未找到可用的视频回放');
return;
}
// 创建线路选择容器
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.right = '20px';
container.style.top = '50%';
container.style.transform = 'translateY(-50%)';
container.style.zIndex = '9999';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '10px';
container.style.backgroundColor = 'rgba(0,0,0,0.7)';
container.style.padding = '15px';
container.style.borderRadius = '8px';
container.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
container.style.maxWidth = '200px';
// 循环所有回放数据
videoList.forEach((video, index) => {
// 获取线路名称和清晰度
const lineSharpness = video.line_sharpness || [];
// 遍历每个清晰度
lineSharpness.forEach((quality, qIndex) => {
const sharpnessName = quality.name || '默认';
const lineName = video.line_name ? `${video.line_name}-${sharpnessName}` : `回放${index + 1}-${sharpnessName}`;
const m3u8Url = quality.url || '';
// 创建线路按钮
const lineButton = document.createElement('div');
lineButton.innerText = lineName;
lineButton.style.padding = '8px 12px';
lineButton.style.backgroundColor = 'rgba(255,255,255,0.1)';
lineButton.style.color = '#fff';
lineButton.style.borderRadius = '4px';
lineButton.style.cursor = 'pointer';
lineButton.style.fontSize = '14px';
lineButton.style.marginBottom = '8px';
lineButton.style.textAlign = 'center';
lineButton.style.transition = 'all 0.2s ease';
lineButton.style.position = 'relative';
// 为第一个按钮添加推荐标志
if (index === 0 && qIndex === 0) {
// 创建红色三角标志
const recommendBadge = document.createElement('div');
recommendBadge.style.position = 'absolute';
recommendBadge.style.top = '0';
recommendBadge.style.left = '0';
recommendBadge.style.width = '0';
recommendBadge.style.height = '0';
recommendBadge.style.borderStyle = 'solid';
recommendBadge.style.borderWidth = '20px 20px 0 0';
recommendBadge.style.borderColor = '#ff4d4f transparent transparent transparent';
recommendBadge.style.borderTopLeftRadius = '4px';
lineButton.appendChild(recommendBadge);
}
// 鼠标悬停效果
lineButton.addEventListener('mouseover', () => {
lineButton.style.backgroundColor = 'rgba(255,255,255,0.2)';
});
lineButton.addEventListener('mouseout', () => {
lineButton.style.backgroundColor = 'rgba(255,255,255,0.1)';
});
// 添加点击复制功能
lineButton.addEventListener('click', () => {
GM_setClipboard(m3u8Url, 'text');
const originalText = lineButton.innerText;
lineButton.innerText = '已复制!';
lineButton.style.backgroundColor = '#28a745';
setTimeout(() => {
lineButton.innerText = originalText;
lineButton.style.backgroundColor = 'rgba(255,255,255,0.1)';
}, 1000);
});
container.appendChild(lineButton);
});
});
// 将容器添加到页面
document.body.appendChild(container);
// 创建提示信息
const messageDiv = document.createElement('div');
messageDiv.innerHTML = `
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9998; background: rgba(0,0,0,0.8); padding: 20px 30px; border-radius: 8px; color: #fff; min-width: 300px; box-shadow: 0 2px 12px rgba(0,0,0,0.15);">
<div style="position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 20px; color: rgba(255,255,255,0.8);" class="close-btn">×</div>
<h2 style="margin: 0 0 15px; font-size: 24px; font-weight: bold; color: #fff; text-align: center; position: relative; overflow: hidden;" class="spotlight-title">
<span style="position: relative; z-index: 1;">视频地址解密成功</span>
<div class="spotlight" style="position: absolute; width: 30px; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent); top: 0; left: -100%; transform: skewX(-25deg); animation: spotlight 3s infinite;"></div>
</h2>
<style>
@keyframes spotlight {
0% { left: -100%; }
100% { left: 200%; }
}
</style>
<div style="text-align: left;">
<p style="margin: 0 0 15px; font-size: 16px; color: rgba(255,255,255,0.9);">您准备下载的直播:${videoTitle}</p>
<p style="margin: 0 0 10px; font-size: 16px; color: rgba(255,255,255,0.9);">直播回放到期时间:${expireTime}</p>
${rawExpireTime ? `<p style="margin: 0 0 10px; font-size: 16px; color: #ff4d4f;">距离回放到期还有:<span id="countdown">${timeLeftStr}</span></p>` : ''}
<p style="margin: 15px 0 0; font-size: 14px; color: rgba(255,255,255,0.7);">请根据自己的需求尽快下载,防止直播回放到期导致无法下载资源。</p>
</div>
</div>
`;
document.body.appendChild(messageDiv);
// 添加倒计时更新功能
if (rawExpireTime) {
const countdownElement = messageDiv.querySelector('#countdown');
const updateCountdown = () => {
const now = new Date().getTime();
const timeLeft = expireDate - now;
if (timeLeft <= 0) {
countdownElement.textContent = '已过期';
clearInterval(countdownInterval);
return;
}
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
countdownElement.textContent = `${days}天${hours}小时${minutes}分钟${seconds}秒`;
};
const countdownInterval = setInterval(updateCountdown, 1000);
// 关闭按钮时清除定时器
const closeBtn = messageDiv.querySelector('.close-btn');
closeBtn.addEventListener('click', () => {
clearInterval(countdownInterval);
messageDiv.remove();
});
} else {
// 无倒计时时的关闭按钮
const closeBtn = messageDiv.querySelector('.close-btn');
closeBtn.addEventListener('click', () => {
messageDiv.remove();
});
}
// 燃放烟花效果
confetti({
particleCount: 100,
spread: 160,
origin: { y: 0.6 }
});
// 移除自动关闭的代码
// setTimeout(() => {
// messageDiv.remove();
// }, 5000);
} catch (e) {
console.error('解析JSON失败:', e);
}
},
onerror: function(error) {
console.error('请求失败:', error);
}
});
} catch (e) {
console.error('解析视频详情失败:', e);
}
},
onerror: function(error) {
console.error('请求视频详情失败:', error);
}
});
}
})();