// ==UserScript==
// @name 资源嗅探器 Pro v4
// @namespace http://tampermonkey.net/
// @version 4.1
// @description 强大的网页资源嗅探工具,支持自动检测、分类展示、预览和下载各类网页资源
// @author CodeBuddy
// @match *://*/*
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
class ResourceSniffer {
constructor() {
// 配置选项
this.config = {
// 支持的资源类型
resourceTypes: {
image: { enabled: true, extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tiff'], icon: '📷' },
video: { enabled: true, extensions: ['mp4', 'webm', 'avi', 'mov', 'flv', 'wmv', 'mkv'], icon: '🎬' },
audio: { enabled: true, extensions: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma'], icon: '🎵' },
document: { enabled: true, extensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv'], icon: '📄' },
other: { enabled: true, icon: '📦' }
},
// 面板位置和大小
panel: {
width: '350px',
height: '600px',
left: '20px',
top: '100px',
opacity: 0.95
},
// 其他配置
maxResources: 500,
ignoreSmallResources: true,
minResourceSize: 1024, // 1KB
updateInterval: 5000 // 5秒更新一次UI
};
// 全局变量
this.resources = new Map(); // 存储嗅探到的资源
this.panelVisible = false; // 面板可见性
this.activeTab = 'all'; // 当前激活的标签
this.panelElement = null; // 面板元素
this.toggleButton = null; // 切换按钮
this.resourceCount = 0; // 资源计数
this.isDragging = false; // 是否正在拖拽
this.dragOffset = { x: 0, y: 0 }; // 拖拽偏移量
this.previewModal = null; // 预览模态框
this.lastUpdateTime = 0; // 上次更新时间
// 初始化
this.init();
}
// 初始化函数
init() {
// 确保文档就绪后初始化
this.checkDocumentReady();
// 拦截请求以嗅探资源
this.interceptRequests();
// 监听页面上的媒体元素
this.monitorMediaElements();
}
// 检查文档是否就绪
checkDocumentReady() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.onDocumentReady());
} else {
// 延迟一点执行,确保body已完全加载
setTimeout(() => this.onDocumentReady(), 300);
}
}
// 文档就绪后执行
onDocumentReady() {
console.log('资源嗅探器 Pro v4 已加载');
// 创建悬浮按钮
this.createToggleButton();
// 创建样式
this.injectStyles();
// 定期更新UI
setInterval(() => this.updateUI(), this.config.updateInterval);
}
// 创建视频播放器
createVideoPlayer(resource) {
// 检查是否已存在播放器
if (document.getElementById('resource-player')) {
document.getElementById('resource-player').remove();
}
// 创建播放器容器
const playerContainer = document.createElement('div');
playerContainer.id = 'resource-player';
playerContainer.className = 'resource-player';
playerContainer.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 1000px;
background: rgba(0, 0, 0, 0.9);
border-radius: 10px;
padding: 10px;
z-index: 9999;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
`;
// 创建关闭按钮
const closeBtn = document.createElement('button');
closeBtn.innerText = '关闭';
closeBtn.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 0, 0, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
z-index: 10;
`;
closeBtn.onclick = () => {
playerContainer.remove();
};
// 创建视频元素
const videoElement = document.createElement('video');
videoElement.controls = true;
videoElement.style.width = '100%';
videoElement.style.height = 'auto';
videoElement.style.borderRadius = '5px';
// 设置视频源
const sourceElement = document.createElement('source');
sourceElement.src = resource.url;
sourceElement.type = 'video/mp4';
videoElement.appendChild(sourceElement);
// 处理m3u8格式
if (resource.url.includes('.m3u8')) {
// 检查是否支持HLS
if (window.Hls) {
const hls = new Hls();
hls.loadSource(resource.url);
hls.attachMedia(videoElement);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
videoElement.play();
});
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('HLS播放错误:', data);
});
} else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
videoElement.src = resource.url;
videoElement.addEventListener('loadedmetadata', () => {
videoElement.play();
});
} else {
alert('您的浏览器不支持HLS播放,请安装HLS插件或使用其他浏览器');
playerContainer.remove();
}
}
// 添加标题
const titleElement = document.createElement('div');
titleElement.innerText = `正在播放: ${resource.name} ${resource.quality !== 'unknown' ? `(${resource.quality})` : ''}`;
titleElement.style.cssText = `
color: white;
margin: 10px 0;
font-weight: bold;
`;
// 添加下载按钮
const downloadBtn = document.createElement('button');
downloadBtn.innerText = '下载';
downloadBtn.style.cssText = `
background: rgba(0, 128, 255, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
margin-right: 10px;
cursor: pointer;
`;
downloadBtn.onclick = () => {
this.downloadResource(resource);
};
// 添加控制栏
const controlBar = document.createElement('div');
controlBar.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
`;
controlBar.appendChild(downloadBtn);
controlBar.appendChild(closeBtn);
// 组装播放器
playerContainer.appendChild(titleElement);
playerContainer.appendChild(videoElement);
playerContainer.appendChild(controlBar);
// 添加到页面
document.body.appendChild(playerContainer);
// 添加HLS支持脚本(如果需要)
if (resource.url.includes('.m3u8') && !window.Hls) {
const hlsScript = document.createElement('script');
hlsScript.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
document.head.appendChild(hlsScript);
hlsScript.onload = () => {
this.createVideoPlayer(resource);
};
}
}
// 注入样式
injectStyles() {
GM_addStyle(`
/* 面板样式 */
#resource-sniffer-panel {
position: fixed;
width: ${this.config.panel.width};
height: ${this.config.panel.height};
left: ${this.config.panel.left};
top: ${this.config.panel.top};
background: #1e1e1e;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
z-index: 30000;
display: flex;
flex-direction: column;
opacity: ${this.config.panel.opacity};
transition: opacity 0.3s;
font-family: 'Microsoft YaHei', Arial, sans-serif;
}
/* 面板头部 */
#sniffer-panel-header {
padding: 10px 15px;
background: #2d2d2d;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
}
#panel-title {
color: white;
font-size: 14px;
font-weight: bold;
}
#panel-controls {
display: flex;
gap: 8px;
}
.panel-btn {
background: none;
border: none;
color: white;
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
transition: background 0.2s;
}
.panel-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
/* 标签栏 */
#sniffer-tabs {
display: flex;
background: #252526;
overflow-x: auto;
white-space: nowrap;
border-bottom: 1px solid #373737;
}
.tab-btn {
padding: 8px 15px;
color: #d4d4d4;
background: none;
border: none;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 5px;
}
.tab-btn.active {
color: white;
background: #1e1e1e;
border-bottom: 2px solid #0078d7;
}
.tab-btn:hover:not(.active) {
background: rgba(255, 255, 255, 0.05);
}
/* 资源列表 */
#resources-container {
flex: 1;
overflow-y: auto;
padding: 10px;
}
#resources-list {
list-style: none;
padding: 0;
margin: 0;
}
.resource-item {
background: #2d2d2d;
border-radius: 6px;
margin-bottom: 10px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.resource-item:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.resource-header {
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
background: #252526;
cursor: pointer;
}
.resource-title {
color: white;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 10px;
}
.resource-size {
color: #999;
font-size: 12px;
margin-right: 10px;
}
.resource-type-badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
color: white;
display: flex;
align-items: center;
gap: 3px;
}
.type-image {
background: #0078d7;
}
.type-video {
background: #00bcf2;
}
.type-audio {
background: #7c7cd9;
}
.type-document {
background: #d83b01;
}
.type-other {
background: #515151;
}
.resource-content {
padding: 10px;
display: none;
}
.resource-preview-container {
width: 100%;
height: 180px;
background: #1e1e1e;
border-radius: 4px;
margin-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
.resource-thumbnail {
width: 100%;
height: 100%;
object-fit: contain;
}
.resource-actions {
display: flex;
gap: 10px;
}
.resource-btn {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
transition: background 0.2s;
}
.preview {
background: #0078d7;
color: white;
}
.preview:hover {
background: #005a9e;
}
.download {
background: #00b42a;
color: white;
}
.download:hover {
background: #008c22;
}
.resource-url {
margin-top: 10px;
padding: 8px;
background: #1e1e1e;
border-radius: 4px;
font-size: 12px;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 空状态 */
#empty-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
color: #666;
text-align: center;
}
#empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 15px;
opacity: 0.3;
}
/* 预览模态框 */
#preview-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 30000;
flex-direction: column;
}
#preview-modal .modal-content {
background: #1e1e1e;
border-radius: 8px;
max-width: 90%;
max-height: 90%;
overflow: hidden;
position: relative;
}
#preview-modal .modal-header {
padding: 10px 15px;
background: #2d2d2d;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
#preview-modal .preview-title {
font-size: 14px;
font-weight: bold;
}
#preview-modal .close-btn {
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
}
#preview-modal .preview-body {
padding: 10px;
max-height: 70vh;
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
}
#preview-modal img, #preview-modal video, #preview-modal audio {
max-width: 100%;
max-height: 70vh;
}
/* 切换按钮 */
#resource-sniffer-toggle {
position: fixed;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #0078d7, #00bcf2);
color: white;
border: none;
cursor: pointer;
font-size: 20px;
z-index: 29999;
box-shadow: 0 5px 15px rgba(0, 120, 215, 0.3);
display: flex;
justify-content: center;
align-items: center;
right: 20px;
bottom: 20px;
transition: all 0.3s;
}
#resource-sniffer-toggle:hover {
transform: scale(1.1);
box-shadow: 0 8px 20px rgba(0, 120, 215, 0.4);
}
#resource-sniffer-toggle .resource-count {
position: absolute;
top: -5px;
right: -5px;
background: #ff3b30;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
border: 2px solid white;
}
/* 自定义样式类 */
.current-video-badge {
background-color: #ff3b30;
color: white;
padding: 2px 5px;
border-radius: 3px;
font-size: 10px;
margin-left: 5px;
}
.priority-video {
background-color: #ff9500 !important;
}
/* 手机端响应式布局 */
@media screen and (max-width: 768px) {
#resource-sniffer-panel {
width: 95% !important;
height: 80% !important;
left: 2.5% !important;
top: 10% !important;
}
.resource-title {
font-size: 14px !important;
max-width: 60% !important;
}
.resource-size {
font-size: 12px !important;
}
.resource-type-badge {
font-size: 12px !important;
padding: 2px 5px !important;
margin-left: 5px !important;
}
.resource-btn {
padding: 6px !important;
font-size: 12px !important;
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #2d2d2d;
}
::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #777;
}
`);
}
// 创建切换按钮
createToggleButton() {
if (!document.body) {
console.error('document.body 不存在,无法创建切换按钮');
return;
}
// 避免重复创建
if (this.toggleButton) {
return;
}
try {
this.toggleButton = document.createElement('button');
this.toggleButton.id = 'resource-sniffer-toggle';
this.toggleButton.innerHTML = `
🕵️
<span class="resource-count">0</span>
`;
this.toggleButton.title = '资源嗅探器 Pro v4';
// 添加点击事件
this.toggleButton.addEventListener('click', () => this.togglePanel());
// 添加到页面
document.body.appendChild(this.toggleButton);
console.log('切换按钮已创建');
} catch (error) {
console.error('创建切换按钮失败:', error);
// 尝试延迟后重试
setTimeout(() => this.createToggleButton(), 500);
}
}
// 切换面板显示/隐藏
togglePanel() {
this.panelVisible = !this.panelVisible;
if (this.panelVisible) {
this.createPanel();
this.panelElement.style.display = 'flex';
} else {
if (this.panelElement) {
this.panelElement.style.display = 'none';
}
}
}
// 创建面板
createPanel() {
if (this.panelElement) {
return;
}
// 确保document.body已加载
if (!document.body) {
console.error('document.body 不存在,无法创建面板');
setTimeout(() => this.createPanel(), 500);
return;
}
try {
// 创建面板元素
this.panelElement = document.createElement('div');
this.panelElement.id = 'resource-sniffer-panel';
this.panelElement.style.display = 'none';
// 面板头部
const header = document.createElement('div');
header.id = 'sniffer-panel-header';
header.innerHTML = `
<div id="panel-title">资源嗅探器 Pro v4</div>
<div id="panel-controls">
<button class="panel-btn" id="minimize-btn" title="最小化">—</button>
<button class="panel-btn" id="refresh-btn" title="刷新">↻</button>
<button class="panel-btn" id="close-btn" title="关闭">×</button>
</div>
`;
// 标签栏
const tabs = document.createElement('div');
tabs.id = 'sniffer-tabs';
// 添加所有标签
let tabsHTML = '<button class="tab-btn active" data-tab="all">全部</button>';
for (const [type, config] of Object.entries(this.config.resourceTypes)) {
if (config.enabled) {
tabsHTML += `<button class="tab-btn" data-tab="${type}">${config.icon} ${type}</button>`;
}
}
tabs.innerHTML = tabsHTML;
// 资源列表容器
const resourcesContainer = document.createElement('div');
resourcesContainer.id = 'resources-container';
// 空状态
const emptyState = document.createElement('div');
emptyState.id = 'empty-state';
emptyState.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
<div>暂无检测到的资源</div>
<div style="font-size: 12px; margin-top: 5px;">访问网页时会自动检测资源</div>
`;
resourcesContainer.appendChild(emptyState);
// 资源列表
const resourcesList = document.createElement('ul');
resourcesList.id = 'resources-list';
resourcesContainer.appendChild(resourcesList);
// 组装面板
this.panelElement.appendChild(header);
this.panelElement.appendChild(tabs);
this.panelElement.appendChild(resourcesContainer);
// 添加到页面
document.body.appendChild(this.panelElement);
// 添加标签点击事件
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
// 移除所有激活状态
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
// 添加当前激活状态
btn.classList.add('active');
// 更新当前标签
this.activeTab = btn.dataset.tab;
// 更新资源列表
this.updateResourceList();
});
});
// 添加面板控制事件
document.getElementById('close-btn').addEventListener('click', () => {
this.panelVisible = false;
this.panelElement.style.display = 'none';
});
document.getElementById('minimize-btn').addEventListener('click', () => {
this.panelElement.style.height = '40px';
tabs.style.display = 'none';
resourcesContainer.style.display = 'none';
});
document.getElementById('refresh-btn').addEventListener('click', () => {
this.updateResourceList();
});
// 添加拖拽功能
header.addEventListener('mousedown', (e) => {
this.isDragging = true;
this.dragOffset.x = e.clientX - this.panelElement.getBoundingClientRect().left;
this.dragOffset.y = e.clientY - this.panelElement.getBoundingClientRect().top;
});
document.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
e.preventDefault();
const x = e.clientX - this.dragOffset.x;
const y = e.clientY - this.dragOffset.y;
this.panelElement.style.left = `${x}px`;
this.panelElement.style.top = `${y}px`;
});
document.addEventListener('mouseup', () => {
this.isDragging = false;
});
console.log('面板已创建');
} catch (error) {
console.error('创建面板失败:', error);
// 尝试延迟后重试
setTimeout(() => this.createPanel(), 500);
}
}
// 更新UI
updateUI() {
const now = Date.now();
if (now - this.lastUpdateTime < this.config.updateInterval) {
return;
}
this.lastUpdateTime = now;
// 更新资源计数
this.updateResourceCount();
// 如果面板可见,更新资源列表
if (this.panelVisible) {
this.updateResourceList();
}
}
// 更新资源计数
updateResourceCount() {
const count = this.resources.size;
this.resourceCount = count;
// 更新按钮上的计数
if (this.toggleButton) {
const countElement = this.toggleButton.querySelector('.resource-count');
if (countElement) {
countElement.textContent = count;
}
}
}
// 更新资源列表
updateResourceList() {
const resourcesList = document.getElementById('resources-list');
const emptyState = document.getElementById('empty-state');
if (!resourcesList || !emptyState) {
return;
}
// 清空列表
resourcesList.innerHTML = '';
// 筛选资源
let filteredResources = Array.from(this.resources.values());
if (this.activeTab !== 'all') {
filteredResources = filteredResources.filter(resource => resource.type === this.activeTab);
}
// 优化排序:视频优先,当前播放视频优先,M3U8/MP4优先,最后按大小排序
const currentVideoUrl = this.getCurrentVideoUrl();
filteredResources.sort((a, b) => {
// 1. 判断是否为视频类型
const isVideoA = a.type === 'video' && (a.extension === 'mp4' || a.extension === 'm3u8' || a.url.includes('.mp4') || a.url.includes('.m3u8'));
const isVideoB = b.type === 'video' && (b.extension === 'mp4' || b.extension === 'm3u8' || b.url.includes('.mp4') || b.url.includes('.m3u8'));
if (isVideoA !== isVideoB) {
return isVideoA ? -1 : 1;
}
// 2. 判断是否为当前播放视频
const isCurrentA = currentVideoUrl && (a.url === currentVideoUrl || a.url.includes(currentVideoUrl.split('?')[0]));
const isCurrentB = currentVideoUrl && (b.url === currentVideoUrl || b.url.includes(currentVideoUrl.split('?')[0]));
if (isCurrentA !== isCurrentB) {
return isCurrentA ? -1 : 1;
}
// 3. M3U8和MP4优先
const isPriorityFormatA = a.extension === 'm3u8' || a.url.includes('.m3u8') || a.extension === 'mp4' || a.url.includes('.mp4');
const isPriorityFormatB = b.extension === 'm3u8' || b.url.includes('.m3u8') || b.extension === 'mp4' || b.url.includes('.mp4');
if (isPriorityFormatA !== isPriorityFormatB) {
return isPriorityFormatA ? -1 : 1;
}
// 4. 最后按大小排序
return b.size - a.size;
});
// 显示空状态或资源列表
if (filteredResources.length === 0) {
emptyState.style.display = 'flex';
} else {
emptyState.style.display = 'none';
// 添加资源项
filteredResources.forEach(resource => {
const resourceItem = this.createResourceItem(resource);
resourcesList.appendChild(resourceItem);
});
}
}
// 创建资源项
createResourceItem(resource) {
const item = document.createElement('li');
item.className = 'resource-item';
// 判断是否为优先视频格式
const isPriorityVideo = resource.type === 'video' &&
(resource.extension === 'mp4' || resource.extension === 'm3u8' ||
resource.url.includes('.mp4') || resource.url.includes('.m3u8'));
// 判断是否为当前播放视频
const currentVideoUrl = this.getCurrentVideoUrl();
const isCurrentVideo = currentVideoUrl &&
(resource.url === currentVideoUrl ||
resource.url.includes(currentVideoUrl.split('?')[0]));
// 资源头部
const header = document.createElement('div');
header.className = 'resource-header';
header.innerHTML = `
<div class="resource-title">${this.truncateText(resource.name, 30)}${isCurrentVideo ? ' <span class="current-video-badge">正在播放</span>' : ''}</div>
<div class="resource-size">${this.formatSize(resource.size)}</div>
<div class="resource-type-badge type-${resource.type} ${isPriorityVideo ? 'priority-video' : ''}">
${isPriorityVideo ? '🎬' : this.config.resourceTypes[resource.type].icon} ${resource.type}${isPriorityVideo ? (resource.extension === 'm3u8' || resource.url.includes('.m3u8') ? ' (M3U8)' : ' (MP4)') : ''}
</div>
`;
// 点击展开/折叠
header.addEventListener('click', () => {
const content = item.querySelector('.resource-content');
if (content) {
content.style.display = content.style.display === 'block' ? 'none' : 'block';
}
});
// 资源内容
const content = document.createElement('div');
content.className = 'resource-content';
// 资源预览
const previewContainer = document.createElement('div');
previewContainer.className = 'resource-preview-container';
// 根据资源类型创建预览
if (resource.type === 'image') {
const img = document.createElement('img');
img.className = 'resource-thumbnail';
img.src = resource.url;
img.alt = resource.name;
previewContainer.appendChild(img);
} else if (resource.type === 'video') {
const video = document.createElement('video');
video.className = 'resource-thumbnail';
video.controls = true;
video.src = resource.url;
previewContainer.appendChild(video);
} else if (resource.type === 'audio') {
const audio = document.createElement('audio');
audio.controls = true;
audio.src = resource.url;
previewContainer.appendChild(audio);
} else {
// 其他类型的资源,显示图标
previewContainer.innerHTML = `
<div style="text-align: center;">
<div style="font-size: 48px;">${this.config.resourceTypes[resource.type].icon}</div>
<div style="margin-top: 10px;">${resource.type.toUpperCase()}</div>
</div>
`;
}
// 资源操作
const actions = document.createElement('div');
actions.className = 'resource-actions';
actions.innerHTML = `
<button class="resource-btn preview" data-url="${resource.url}" data-type="${resource.type}" data-name="${resource.name}">预览</button>
<button class="resource-btn download" data-url="${resource.url}" data-name="${resource.name}">下载</button>
`;
// 添加操作事件
actions.querySelector('.preview').addEventListener('click', (e) => {
e.stopPropagation();
this.previewResource(resource);
});
actions.querySelector('.download').addEventListener('click', (e) => {
e.stopPropagation();
this.downloadResource(resource);
});
// 资源URL
const url = document.createElement('div');
url.className = 'resource-url';
url.textContent = resource.url;
// 组装资源项
content.appendChild(previewContainer);
content.appendChild(actions);
content.appendChild(url);
item.appendChild(header);
item.appendChild(content);
return item;
}
// 预览资源
previewResource(resource) {
// 创建预览模态框
if (!this.previewModal) {
this.createPreviewModal();
}
// 设置预览内容
const previewBody = document.querySelector('#preview-modal .preview-body');
const previewTitle = document.querySelector('#preview-modal .preview-title');
if (previewBody && previewTitle) {
previewTitle.textContent = resource.name;
// 根据资源类型创建预览内容
if (resource.type === 'image') {
previewBody.innerHTML = `
<img src="${resource.url}" alt="${resource.name}">
`;
} else if (resource.type === 'video') {
previewBody.innerHTML = `
<video controls autoplay>
<source src="${resource.url}" type="video/${resource.extension}">
</video>
`;
} else if (resource.type === 'audio') {
previewBody.innerHTML = `
<audio controls autoplay>
<source src="${resource.url}" type="audio/${resource.extension}">
</audio>
`;
} else {
// 其他类型资源,显示信息和下载按钮
previewBody.innerHTML = `
<div style="text-align: center; color: white;">
<div style="font-size: 64px; margin-bottom: 20px;">${this.config.resourceTypes[resource.type].icon}</div>
<h3>${resource.name}</h3>
<p>类型: ${resource.type}</p>
<p>大小: ${this.formatSize(resource.size)}</p>
<button class="resource-btn download" style="margin-top: 20px;" data-url="${resource.url}" data-name="${resource.name}">下载</button>
</div>
`;
// 添加下载事件
previewBody.querySelector('.download').addEventListener('click', () => {
this.downloadResource(resource);
});
}
}
// 显示预览模态框
this.previewModal.style.display = 'flex';
}
// 创建预览模态框
createPreviewModal() {
if (this.previewModal) {
return;
}
this.previewModal = document.createElement('div');
this.previewModal.id = 'preview-modal';
this.previewModal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<div class="preview-title"></div>
<button class="close-btn">×</button>
</div>
<div class="preview-body"></div>
</div>
`;
// 添加关闭事件
this.previewModal.querySelector('.close-btn').addEventListener('click', () => {
this.previewModal.style.display = 'none';
});
// 点击模态框外部关闭
this.previewModal.addEventListener('click', (e) => {
if (e.target === this.previewModal) {
this.previewModal.style.display = 'none';
}
});
// 添加到页面
document.body.appendChild(this.previewModal);
}
// 下载资源
downloadResource(resource) {
try {
console.log(`开始下载资源: ${resource.name}`);
// 判断资源类型
if (resource.url.includes('.m3u8') || resource.type === 'video') {
// 视频资源,使用高级下载方法
this.m3u8Download(resource);
} else {
// 普通资源下载
GM_download({
url: resource.url,
name: resource.name,
saveAs: true,
headers: resource.headers || {},
onprogress: (event) => {
if (event) {
const progress = ((event.loaded / event.total) * 100).toFixed(2);
console.log(`下载进度: ${progress}% - ${resource.name}`);
// 这里可以添加进度显示逻辑
}
},
onload: () => {
console.log(`资源下载完成: ${resource.name}`);
},
onerror: (error) => {
console.error(`下载资源失败: ${resource.name}`, error);
// 尝试使用备用下载方法
this.fallbackDownload(resource);
}
});
}
} catch (error) {
console.error(`下载资源失败: ${resource.name}`, error);
this.fallbackDownload(resource);
}
}
// 降级下载方案
fallbackDownload(resource) {
const a = document.createElement('a');
a.href = resource.url;
a.download = resource.name;
// 添加请求头信息
if (resource.headers) {
// 注意: 浏览器环境下无法通过a标签设置请求头
console.warn('无法为a标签下载设置请求头,可能导致下载失败');
}
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// m3u8视频下载
m3u8Download(resource) {
console.log(`开始解析m3u8视频: ${resource.name}`);
// 创建下载进度条
this.createDownloadProgress(resource.id, resource.name);
// 初始化变量
const chunks = [];
const chunkSize = 5 * 1024 * 1024; // 5MB per chunk
let totalSize = 0;
let downloadedSize = 0;
let isEncrypted = false;
let keyUrl = null;
let iv = null;
// 获取m3u8文件
this.fetchWithHeaders(resource.url, resource.headers || {})
.then(response => response.text())
.then(playlist => {
// 解析m3u8播放列表
const lines = playlist.split('\n').filter(line => line.trim() !== '');
// 检查是否加密
for (const line of lines) {
if (line.startsWith('#EXT-X-KEY')) {
isEncrypted = true;
const keyMatch = line.match(/URI="([^"]+)"/);
if (keyMatch) {
keyUrl = new URL(keyMatch[1], resource.url).href;
}
const ivMatch = line.match(/IV=([0-9A-Fa-f]+)/);
if (ivMatch) {
iv = ivMatch[1];
}
break;
}
}
// 提取TS片段
const tsUrls = [];
for (let i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#')) {
// 处理相对路径
tsUrls.push(new URL(lines[i], resource.url).href);
}
}
totalSize = tsUrls.length;
console.log(`找到 ${totalSize} 个TS片段`);
// 下载TS片段
const downloadPromises = tsUrls.map((tsUrl, index) => {
return this.fetchWithHeaders(tsUrl, resource.headers || {})
.then(response => response.arrayBuffer())
.then(buffer => {
downloadedSize++;
const progress = ((downloadedSize / totalSize) * 100).toFixed(2);
console.log(`视频片段下载进度: ${progress}%`);
this.updateDownloadProgress(resource.id, progress);
// 如果加密,进行解密
if (isEncrypted && keyUrl) {
// 这里应该实现解密逻辑
console.warn('视频已加密,解密功能待实现');
return buffer;
}
return buffer;
});
});
// 等待所有片段下载完成
Promise.all(downloadPromises)
.then(buffers => {
console.log(`所有TS片段下载完成,开始合并`);
// 合并所有片段
const mergedBuffer = new Uint8Array(buffers.reduce((acc, buffer) => acc + buffer.byteLength, 0));
let offset = 0;
buffers.forEach(buffer => {
mergedBuffer.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
});
// 创建下载链接
const blob = new Blob([mergedBuffer], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = resource.name.replace(/\.m3u8$/, '.mp4');
document.body.appendChild(a);
a.click();
// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.removeDownloadProgress(resource.id);
}, 1000);
})
.catch(error => {
console.error('下载TS片段失败:', error);
this.updateDownloadProgress(resource.id, '错误', true);
});
})
.catch(error => {
console.error('获取m3u8文件失败:', error);
this.updateDownloadProgress(resource.id, '错误', true);
});
}
// 创建带请求头的fetch
fetchWithHeaders(url, headers) {
return fetch(url, {
headers: headers,
credentials: 'include'
});
}
// 创建下载进度条
createDownloadProgress(id, name) {
// 检查是否已存在进度条
if (document.getElementById(`download-progress-${id}`)) {
return;
}
const progressContainer = document.createElement('div');
progressContainer.id = `download-progress-${id}`;
progressContainer.className = 'download-progress';
progressContainer.innerHTML = `
<div class="progress-title">${this.truncateText(name, 30)}</div>
<div class="progress-bar">
<div class="progress-value">0%</div>
</div>
`;
// 添加到面板
if (this.panelElement) {
this.panelElement.appendChild(progressContainer);
} else {
document.body.appendChild(progressContainer);
}
}
// 更新下载进度
updateDownloadProgress(id, progress, isError = false) {
const progressContainer = document.getElementById(`download-progress-${id}`);
if (!progressContainer) return;
const progressBar = progressContainer.querySelector('.progress-bar');
const progressValue = progressContainer.querySelector('.progress-value');
if (progressBar && progressValue) {
if (isError) {
progressBar.style.backgroundColor = '#ff4d4f';
progressValue.textContent = '下载失败';
} else {
progressBar.style.width = `${progress}%`;
progressValue.textContent = `${progress}%`;
}
}
}
// 移除下载进度条
removeDownloadProgress(id) {
const progressContainer = document.getElementById(`download-progress-${id}`);
if (progressContainer) {
progressContainer.remove();
}
}
// 拦截请求
interceptRequests() {
// 保存原始fetch和XMLHttpRequest
const originalFetch = window.fetch;
const originalXhrOpen = XMLHttpRequest.prototype.open;
// 重写fetch
window.fetch = async (url, options) => {
// 处理请求
this.handleRequest(url);
// 执行原始fetch
return originalFetch.apply(this, arguments);
};
// 重写XMLHttpRequest.open
XMLHttpRequest.prototype.open = function(method, url) {
// 处理请求
this._url = url;
this.addEventListener('load', () => {
if (this.status >= 200 && this.status < 300) {
// 尝试获取响应大小
const size = this.getResponseHeader('Content-Length') || 0;
this.handleRequest(this._url, parseInt(size));
}
});
// 执行原始open
originalXhrOpen.apply(this, arguments);
};
}
// 处理请求
handleRequest(url, size = 0) {
// 跳过本身的请求
if (url.includes('resource-sniffer')) {
return;
}
// 针对特定网站的视频URL处理
const videoUrl = this.extractVideoUrl(url);
if (videoUrl) {
// 处理提取到的视频URL
const resourceInfo = this.getResourceInfo(videoUrl, size);
if (resourceInfo) {
// 添加到资源列表
this.addResource(resourceInfo);
}
return;
}
// 检查是否为有效的资源URL
const resourceInfo = this.getResourceInfo(url, size);
if (resourceInfo) {
// 添加到资源列表
this.addResource(resourceInfo);
}
}
// 提取视频URL(针对特定网站)
extractVideoUrl(url) {
// 西瓜视频
if (url.includes('ixigua.com')) {
// 处理西瓜视频API请求
if (url.includes('ixigua.com/api/albumv2/') ||
url.includes('ixigua.com/api/videov2/pseries_more_v2') ||
url.includes('ixigua.com/api/mixVideo/')) {
return url;
}
// 普通西瓜视频URL
return url;
}
// 抖音
if (url.includes('douyin.com')) {
// 抖音API处理
if (url.includes('v3-web-prime.douyinvod.com/video/') ||
url.includes('v26-web-prime.douyinvod.com/video/') ||
url.includes('douyin.com/aweme/v1/play/?file_id=')) {
return url;
}
return url;
}
// YouTube
if (url.includes('youtube.com') || url.includes('youtu.be')) {
// 提取真实视频URL
if (url.includes('youtube.com/watch')) {
const videoId = new URL(url).searchParams.get('v');
if (videoId) {
return `https://www.youtube.com/get_video_info?video_id=${videoId}`;
}
}
return url;
}
// B站
if (url.includes('bilibili.com')) {
// B站API处理
if (url.includes('api.bilibili.com/x/player/playurl')) {
return url;
}
// 视频页面URL
if (url.includes('bilibili.com/video/') || url.includes('bilibili.com/bangumi/')) {
// 提取avid和cid
const match = url.match(/(av\d+|BV\w+)/);
if (match) {
const aid = match[0];
return `https://api.bilibili.com/x/player/playurl?avid=${aid}&cid=0&qn=120`;
}
}
return url;
}
// 央视网
if (url.includes('cntv')) {
if (url.includes('/asp/')) {
// 央视网URL特殊处理
const realUrl = url.replace(/.+?cntv.*?\/asp\/.*?hls\/(.*)/, 'https://hls.cntv.myalicdn.com/asp/hls/$1');
return realUrl;
}
return url;
}
// 通用视频格式检测
const videoExtensions = ['.m3u8', '.mp4', '.webm', '.flv', '.avi', '.mov',
'.f4v', '.mkv', '.rmvb', '.wmv', '.3gp', '.ts'];
for (const ext of videoExtensions) {
if (url.includes(ext)) {
return url;
}
}
// 处理没有扩展名但可能是视频的URL
const videoKeywords = ['video', 'stream', 'media', 'play', 'source', 'file', 'vod'];
for (const keyword of videoKeywords) {
if (url.toLowerCase().includes(keyword)) {
// 检查是否为PHP请求但没有明显视频扩展名
if (url.includes('.php') && !url.includes('.jpg') && !url.includes('.png') &&
!url.includes('.gif') && !url.includes('.css') && !url.includes('.js')) {
return url + '&type=.m3u8'; // 尝试添加m3u8格式参数
}
return url;
}
}
return null;
}
// 获取资源信息
getResourceInfo(url, size = 0) {
try {
const parsedUrl = new URL(url);
const pathname = parsedUrl.pathname;
let filename = pathname.split('/').pop() || 'unknown';
let extension = filename.split('.').pop().toLowerCase();
const siteInfo = this.getSiteInfo(url);
// 确定资源类型
let type = 'other';
let quality = 'unknown';
let duration = 0;
// 视频扩展名和关键词
const videoExtensions = ['.m3u8', '.mp4', '.webm', '.flv', '.avi', '.mov',
'.f4v', '.mkv', '.rmvb', '.wmv', '.3gp', '.ts'];
const videoKeywords = ['video', 'stream', 'media', 'play', 'source', 'vod', 'watch'];
// 音频扩展名
const audioExtensions = ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.opus'];
// 图片扩展名
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
// 文档扩展名
const documentExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
// 压缩包扩展名
const archiveExtensions = ['.zip', '.rar', '.7z', '.tar', '.gz'];
// 特殊处理没有扩展名但可能是视频的URL
if (extension === filename) {
// 检查是否为视频URL
if (url.includes('.m3u8') || videoKeywords.some(keyword => url.toLowerCase().includes(keyword))) {
type = 'video';
extension = 'mp4'; // 假设默认视频格式
}
} else {
// 根据扩展名确定资源类型
if (videoExtensions.includes('.' + extension)) {
type = 'video';
} else if (audioExtensions.includes('.' + extension)) {
type = 'audio';
} else if (imageExtensions.includes('.' + extension)) {
type = 'image';
} else if (documentExtensions.includes('.' + extension)) {
type = 'document';
} else if (archiveExtensions.includes('.' + extension)) {
type = 'archive';
} else {
// 检查是否为视频URL关键词
if (videoKeywords.some(keyword => url.toLowerCase().includes(keyword))) {
type = 'video';
extension = 'mp4';
}
}
}
// 额外检查:如果URL包含视频相关关键词但类型不是视频
if (type !== 'video' && (url.includes('.m3u8') || videoKeywords.some(keyword => url.toLowerCase().includes(keyword)))) {
type = 'video';
extension = 'mp4';
}
// 尝试提取视频质量
if (type === 'video') {
if (url.includes('quality')) {
const qualityMatch = url.match(/quality=(\d+)/);
if (qualityMatch) {
quality = `${qualityMatch[1]}p`;
}
} else if (url.includes('resolution')) {
const resolutionMatch = url.match(/resolution=(\d+x\d+)/);
if (resolutionMatch) {
quality = resolutionMatch[1];
}
} else if (url.includes('1080') || url.includes('fhd')) {
quality = '1080p';
} else if (url.includes('720') || url.includes('hd')) {
quality = '720p';
} else if (url.includes('480') || url.includes('sd')) {
quality = '480p';
} else if (url.includes('360')) {
quality = '360p';
}
}
// 网站特定处理
let headers = {};
if (siteInfo.name === 'ixigua') {
headers.Referer = 'https://www.ixigua.com/';
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36';
} else if (siteInfo.name === 'douyin') {
headers.Referer = 'https://www.douyin.com/';
headers['User-Agent'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';
} else if (siteInfo.name === 'cntv') {
headers.Referer = 'https://tv.cctv.com/';
headers['Origin'] = 'https://tv.cctv.com';
// 央视网特定处理
if (url.includes('.m3u8')) {
// 处理不同分辨率
if (url.includes('main.m3u8')) {
// 提供多种分辨率选项
const url720p = url.replace(/main.m3u8.*/, '1200.m3u8').replace('hls/main/', 'hls/1200/');
const url1080p = url.replace(/main.m3u8.*/, '2000.m3u8').replace('hls/main/', 'hls/2000/');
// 这里可以在资源信息中添加多个分辨率选项
}
}
} else if (siteInfo.name === 'javplayer') {
headers.Referer = 'https://javplayer.me/';
} else if (siteInfo.name === 'aliyundrive') {
headers.Referer = 'https://www.aliyundrive.com/';
} else if (siteInfo.name === 'bilibili') {
headers.Referer = 'https://www.bilibili.com/';
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36';
} else if (siteInfo.name === 'youtube') {
headers.Referer = 'https://www.youtube.com/';
}
// 添加通用请求头以提高兼容性
if (!headers['User-Agent']) {
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36';
}
headers['Accept-Language'] = 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7';
// 忽略小资源
if (this.config.ignoreSmallResources && size > 0 && size < this.config.minResourceSize) {
// 视频资源即使小也不忽略
if (type !== 'video') {
return null;
}
}
// 优化文件名
if (filename === 'unknown' || filename.length > 100) {
filename = `${type}_${Date.now()}.${extension}`;
} else if (!filename.includes('.')) {
filename = `${filename}.${extension}`;
}
return {
id: url + Date.now().toString(), // 确保ID唯一
url: url,
name: filename,
extension: extension,
type: type,
size: size,
duration: duration,
quality: quality,
timestamp: Date.now(),
// 添加网站特定信息
site: siteInfo.name,
siteIcon: siteInfo.icon,
siteCategory: siteInfo.category,
// 添加请求头信息
headers: headers
};
} catch (error) {
console.error('解析URL失败:', error);
return null;
}
}
// 获取网站信息
getSiteInfo(url) {
let name = 'other';
let icon = '🔍';
let category = 'general';
if (url.includes('ixigua.com')) {
name = 'ixigua';
icon = '🍉';
category = 'video';
} else if (url.includes('douyin.com') || url.includes('douyinvod.com')) {
name = 'douyin';
icon = '🎵';
category = 'video';
} else if (url.includes('youtube.com') || url.includes('youtu.be')) {
name = 'youtube';
icon = '▶️';
category = 'video';
} else if (url.includes('bilibili.com') || url.includes('bilibili.tv')) {
name = 'bilibili';
icon = '📱';
category = 'video';
} else if (url.includes('cntv') || url.includes('cctv.com')) {
name = 'cntv';
icon = '📺';
category = 'video';
} else if (url.includes('javplayer.me')) {
name = 'javplayer';
icon = '🎬';
category = 'video';
} else if (url.includes('aliyundrive.com')) {
name = 'aliyundrive';
icon = '☁️';
category = 'storage';
} else if (url.includes('weibo.cn') || url.includes('weibo.com')) {
name = 'weibo';
icon = '🐦';
category = 'social';
} else if (url.includes('qq.com') || url.includes('qzone.qq.com') || url.includes('v.qq.com')) {
name = 'qq';
icon = '🐧';
category = 'video';
} else if (url.includes('music.163.com') || url.includes('netease.com')) {
name = 'netease';
icon = '🎶';
category = 'music';
} else if (url.includes('qqmusic.qq.com')) {
name = 'qqmusic';
icon = '🎵';
category = 'music';
} else if (url.includes('spotify.com')) {
name = 'spotify';
icon = '🎧';
category = 'music';
} else if (url.includes('iwara.tv')) {
name = 'iwara';
icon = '🎮';
category = 'video';
} else if (url.includes('telegram.org') || url.includes('t.me')) {
name = 'telegram';
icon = '✉️';
category = 'social';
} else if (url.includes('github.com')) {
name = 'github';
icon = '💻';
category = 'code';
}
return {
name: name,
icon: icon,
category: category
};
}
// 添加资源
addResource(resource) {
// 检查是否已存在
if (this.resources.has(resource.id)) {
return;
}
// 检查是否超过最大资源数
if (this.resources.size >= this.config.maxResources) {
// 删除最早添加的资源
const oldestResource = Array.from(this.resources.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
this.resources.delete(oldestResource[0]);
}
// 添加资源
this.resources.set(resource.id, resource);
// 更新UI
this.updateUI();
}
// 监听媒体元素
monitorMediaElements() {
// 定期检查新的媒体元素
setInterval(() => {
// 检查图片
document.querySelectorAll('img:not([data-sniffed])').forEach(img => {
img.dataset.sniffed = 'true';
const url = img.src;
this.handleRequest(url);
});
// 检查视频
document.querySelectorAll('video:not([data-sniffed])').forEach(video => {
video.dataset.sniffed = 'true';
// 检查视频源
video.querySelectorAll('source').forEach(source => {
const url = source.src;
this.handleRequest(url);
});
// 如果视频有直接src
if (video.src) {
this.handleRequest(video.src);
}
// 监听视频属性变化
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'src' && video.src) {
this.handleRequest(video.src);
}
});
});
observer.observe(video, { attributes: true });
});
// 检查音频
document.querySelectorAll('audio:not([data-sniffed])').forEach(audio => {
audio.dataset.sniffed = 'true';
// 检查音频源
audio.querySelectorAll('source').forEach(source => {
const url = source.src;
this.handleRequest(url);
});
// 如果音频有直接src
if (audio.src) {
this.handleRequest(audio.src);
}
});
// 检查iframe中的媒体元素
document.querySelectorAll('iframe:not([data-sniffed])').forEach(iframe => {
try {
iframe.dataset.sniffed = 'true';
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) {
// 检查iframe中的图片
iframeDoc.querySelectorAll('img:not([data-sniffed])').forEach(img => {
img.dataset.sniffed = 'true';
const url = img.src;
this.handleRequest(url);
});
// 检查iframe中的视频
iframeDoc.querySelectorAll('video:not([data-sniffed])').forEach(video => {
video.dataset.sniffed = 'true';
const url = video.src;
this.handleRequest(url);
});
}
} catch (e) {
// 跨域iframe无法访问,忽略
}
});
}, 2000);
}
// 获取当前播放的视频URL
getCurrentVideoUrl() {
try {
// 检查页面中是否有正在播放的视频
const videos = document.querySelectorAll('video');
for (const video of videos) {
if (!video.paused) {
return video.currentSrc || video.src;
}
}
// 如果没有正在播放的视频,检查是否有最近加载的视频
if (videos.length > 0) {
return videos[videos.length - 1].currentSrc || videos[videos.length - 1].src;
}
return null;
} catch (error) {
console.error('获取当前播放视频URL失败:', error);
return null;
}
}
// 格式化大小
formatSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 截断文本
truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
}
// 初始化资源嗅探器
window.addEventListener('load', () => {
setTimeout(() => {
const sniffer = new ResourceSniffer();
// 为了测试,直接显示面板
sniffer.panelVisible = true;
sniffer.createPanel();
sniffer.panelElement.style.display = 'flex';
}, 1000);
});