// ==UserScript==
// @name 资源嗅探
// @namespace https://staybrowser.com/
// @version 1.1.1
// @description 资源嗅探工具,支持 M3U8 解析下载,优化 UI,增强交互体验
// @author GaryIndex
// @license telegram @GaryIndex
// @match *://*/*
// @grant none
// @icon https://raw.githubusercontent.com/GaryIndex/GaryIndex/refs/heads/main/GaryIndex.JPG
// ==/UserScript==
(() => {
'use strict';
const snifferBtn = document.createElement('button');
snifferBtn.innerText = '资源嗅探';
Object.assign(snifferBtn.style, {
position: 'fixed', bottom: '20px', right: '20px', zIndex: '10000',
padding: '12px 20px', fontSize: '16px', border: 'none',
borderRadius: '25px', backgroundColor: '#007aff', color: '#fff',
cursor: 'pointer', boxShadow: '0 4px 8px rgba(0,0,0,0.2)'
});
const modal = document.createElement('div');
Object.assign(modal.style, {
position: 'fixed', width: '90%', height: '50vh', left: '5%', top: '25vh',
backgroundColor: '#fff', borderRadius: '12px', boxShadow: '0 4px 10px rgba(0,0,0,0.3)',
display: 'none', flexDirection: 'column', zIndex: '10001', overflow: 'hidden'
});
const modalHeader = document.createElement('div');
Object.assign(modalHeader.style, {
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '12px 16px', fontSize: '16px', fontWeight: 'bold'
});
modalHeader.innerHTML = `<span>选择要下载的资源</span><span style="cursor:pointer;font-weight:bold;color:#007aff;">关闭</span>`;
modalHeader.lastElementChild.onclick = () => { modal.style.display = 'none'; snifferBtn.style.display = 'block'; };
const categories = ['全部', '视频', '音频', '图片', '文档', '文件扩展', '其他'];
let activeCategoryIndex = 0;
const categoryBar = document.createElement('div');
Object.assign(categoryBar.style, {
display: 'flex', overflowX: 'auto', padding: '10px', gap: '10px'
});
categories.forEach((cat, index) => {
const btn = document.createElement('button');
btn.innerText = cat;
btn.dataset.index = index;
Object.assign(btn.style, {
padding: '8px 12px', border: 'none', borderRadius: '20px',
backgroundColor: index === 0 ? '#007aff' : '#f0f0f0',
color: index === 0 ? '#fff' : '#000',
cursor: 'pointer', whiteSpace: 'nowrap'
});
btn.onclick = (e) => {
activeCategoryIndex = parseInt(e.target.dataset.index);
filterResources(cat);
};
categoryBar.appendChild(btn);
});
const resourceList = document.createElement('div');
Object.assign(resourceList.style, { flex: 1, overflowY: 'auto', padding: '10px' });
modal.appendChild(modalHeader);
modal.appendChild(categoryBar);
modal.appendChild(resourceList);
document.body.appendChild(snifferBtn);
document.body.appendChild(modal);
snifferBtn.onclick = () => {
updateResourceList();
modal.style.display = 'flex';
snifferBtn.style.display = 'none';
};
let allResources = [];
function updateResourceList() {
allResources = [];
document.querySelectorAll('a, source, link, video, audio').forEach(el => {
const url = el.href || el.src;
if (url) {
let type = '其他';
if (url.match(/\.(mp4|avi|mov|mkv|webm|flv|wmv|mpeg|mpg|3gp|ts|rm|rmvb|ogv|asf|vob|f4v|m4v|qt|h264|hevc|mpeg-2|mpeg-4|divx|xvid|mov|mts|tp|dat|yuv|m3u8|mkv|webm|hls|mpd|dash|ts|f4v)$/)) type = '视频';
else if (url.match(/\.(mp3|wav|aac|flac|ogg|wma|m4a|opus|alac|ape|dsd|pcm|aiff|au|mid|midi|amr|caf|voc|ra|rm|mpc|tta)$/)) type = '音频';
else if (url.match(/\.(jpeg|jpg|png|gif|bmp|tiff|tif|webp|heif|heic|jpeg 2000|jp2|j2k|apng|tga|tpic|dds|exr|hdr|raw|svg|ai|eps|pdf|cdr|fig|skp|psd|xcf|ico|icns|stl|obj|ply|dicom|shp|pcx|pict|pct|iff|jbig|jbg|sgi)$/)) type = '图片';
else if (url.match(/\.(.py|.js|.java|.c|.cpp|.cc|.cxx|.cs|.go|.rb|.php|.swift|.kt|.kts|.ts|.html|.htm|.css|.json|.xml|.sql|.r|.m|.sh|.bash|.ps1|.rs|.lua|.hs|.scala|.tex|.md|.vhd|.vhdl|.asm|.pl|.dart|.el|.erl|.ex|.exs|.jl|.lisp|.ml|.nim|.pas|.pde|.psm1|.rpy|.sml|.tcl|.v|.vbs|.wsf|.yml|.yaml|.coffee|.graphql|.sh|.sql|.scss|.rmd|.styl|.pug|.vue|.handlebars|.twig|.hbs|.asp|.aspx|.cgi|.pl|.psd|.ai|.indd|.abap|.actionscript|.ada|.awk|.batch|.bc|.bh|.bzl|.capnp|.clj|.cljc|.cobol|.coffee|.cql|.cshtml|.cu|.d|.dats|.db|.dcm|.dif|.dtd|.dylib|.f|.f90|.fd|.fxml|.glsl|.h|.hxx|.hpp|.hx|.idl|.inl|.install|.java|.jl|.l|.lisp|.liquid|.lua|.m4|.makefile|.map|.maven|.ml|.mli|.nim|.ninja|.nvm|.objc|.pl|.pm|.ps|.puml|.py|.q|.r|.rexx|.rst|.rs|.scala|.scm|.sh|.shtml|.sml|.sol|.ss|.svg|.tcl|.tex|.ts|.tsx|.v|.vhdl|.vim|.xhtml|.xml|.xsl|.yaml|.yml)$/)) type = '文件扩展';
else if (url.match(/\.(pdf|doc|docx|ppt|pptx|xls|xlsx|txt|rtf|odt|ods|odp|epub|mobi|azw3|chm|djvu|tex|md|html|xps|pages|key|numbers|csv|tsv|epub3|fb2|azw|abw)$/)) type = '文档';
allResources.push({ name: formatName(url), url, type });
}
});
filterResources(categories[activeCategoryIndex]);
}
function filterResources(category) {
resourceList.innerHTML = '';
const filtered = category === '全部' ? allResources : allResources.filter(res => res.type === category);
if (filtered.length === 0) {
resourceList.innerHTML = `<p style="color:#666; text-align:center;">暂无资源可供下载</p>`;
} else {
filtered.forEach(res => {
if (!res.name) return;
const item = document.createElement('div');
Object.assign(item.style, {
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '8px'
});
const name = document.createElement('span');
name.innerText = res.name;
name.style.flex = '1';
name.style.textAlign = 'left';
const downloadBtn = document.createElement('button');
downloadBtn.innerText = '下载';
Object.assign(downloadBtn.style, {
padding: '6px 12px', border: 'none', borderRadius: '15px',
backgroundColor: '#007aff', color: '#fff', cursor: 'pointer'
});
downloadBtn.onclick = () => downloadResource(res.url, res.name);
item.appendChild(name);
item.appendChild(downloadBtn);
resourceList.appendChild(item);
});
}
categoryBar.querySelectorAll('button').forEach((btn, index) => {
btn.style.backgroundColor = index === activeCategoryIndex ? '#007aff' : '#f0f0f0';
btn.style.color = index === activeCategoryIndex ? '#fff' : '#000';
});
setTimeout(() => {
categoryBar.scrollLeft = categoryBar.children[activeCategoryIndex].offsetLeft - 20;
}, 0);
}
function downloadResource(url, filename) {
if (url.endsWith('.m3u8')) {
downloadM3U8(url, filename);
return;
}
fetch(url)
.then(response => {
const contentType = response.headers.get('content-type') || '';
return response.blob().then(blob => ({ blob, contentType }));
})
.then(({ blob, contentType }) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
const forcedExtensions = [
'.md', '.json', '.xml', '.csv', '.yaml', '.yml', '.sql',
'.sh', '.py', '.js', '.ts', '.html', '.css', '.jsx', '.tsx',
'.c', '.cpp', '.java', '.go', '.rb', '.php', '.swift', '.kt',
'.r', '.lua', '.pl', '.dart', '.scala', '.vhd', '.asm', '.ps1'
];
const extIndex = filename.lastIndexOf('.');
let ext = extIndex !== -1 ? filename.slice(extIndex).toLowerCase() : '';
const randomNum = Math.floor(100 + Math.random() * 900);
if (!ext) {
ext = '.txt';
filename += ext;
}
if (!filename.trim()) {
filename = `downloaded_file${randomNum}.txt`;
}
if (forcedExtensions.includes(ext) || contentType.startsWith('text/')) {
a.download = filename;
} else {
a.download = filename;
}
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
})
.catch(() => alert('资源下载失败'));
}
function downloadM3U8(url, filename) {
fetch(url)
.then(response => response.text())
.then(data => {
const m3u8Blob = new Blob([data], { type: 'application/x-mpegURL' });
const m3u8Url = URL.createObjectURL(m3u8Blob);
const a = document.createElement('a');
a.href = m3u8Url;
a.download = filename.endsWith('.m3u8') ? filename : filename + '.m3u8';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
})
.catch(() => alert('M3U8 下载失败'));
}
function formatName(url) {
const name = url.split('/').pop().split('?')[0];
if (name.length <= 15) return name;
const extIndex = name.lastIndexOf('.');
if (extIndex !== -1) {
const ext = name.slice(extIndex);
const base = name.slice(0, 15);
return base + ext;
}
return name.slice(0, 15);
}
})();