资源嗅探

资源嗅探工具,支持 M3U8 解析下载,优化 UI,增强交互体验

  1. // ==UserScript==
  2. // @name 资源嗅探
  3. // @namespace https://staybrowser.com/
  4. // @version 1.1.2
  5. // @description 资源嗅探工具,支持 M3U8 解析下载,优化 UI,增强交互体验
  6. // @author GaryIndex
  7. // @license telegram @GaryIndex
  8. // @match *://*/*
  9. // @grant none
  10. // @icon https://raw.githubusercontent.com/GaryIndex/GaryIndex/refs/heads/main/GaryIndex.JPG
  11. // ==/UserScript==
  12. (() => {
  13. 'use strict';
  14. const snifferBtn = document.createElement('button');
  15. snifferBtn.innerText = '资源嗅探';
  16. 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)', fontWeight: 'bold' });
  17. const modal = document.createElement('div');
  18. 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', fontFamily: 'Arial, sans-serif', fontSize: '14px' });
  19. const modalHeader = document.createElement('div');
  20. Object.assign(modalHeader.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', fontSize: '16px', fontWeight: 'bold', color: '#000' });
  21. const headerImage = document.createElement('img');
  22. headerImage.src = 'https://raw.githubusercontent.com/GaryIndex/GaryIndex/refs/heads/main/GaryIndex.JPG';
  23. headerImage.alt = '资源图标';
  24. Object.assign(headerImage.style, { width: '30px', height: '30px', marginRight: '10px', objectFit: 'cover', borderRadius: '5px' });
  25. const headerText = document.createElement('span');
  26. headerText.innerText = '选择要下载的资源';
  27. Object.assign(headerText.style, { display: 'inline-block', lineHeight: '30px' });
  28. const closeBtn = document.createElement('span');
  29. closeBtn.innerText = '关闭';
  30. Object.assign(closeBtn.style, { cursor: 'pointer', fontWeight: 'bold', color: '#007aff' });
  31. closeBtn.onclick = () => { modal.style.display = 'none'; snifferBtn.style.display = 'block'; };
  32. const leftContent = document.createElement('div');
  33. leftContent.style.display = 'flex'; leftContent.style.alignItems = 'center';
  34. leftContent.appendChild(headerImage);
  35. leftContent.appendChild(headerText);
  36. modalHeader.appendChild(leftContent);
  37. modalHeader.appendChild(closeBtn);
  38. const categories = ['全部', '视频', '音频', '图片', '文档', '扩展', '其他'];
  39. let activeCategoryIndex = 0;
  40. const categoryBar = document.createElement('div');
  41. Object.assign(categoryBar.style, { display: 'flex', overflowX: 'auto', padding: '10px', gap: '10px', marginBottom: '10px' });
  42. const style = document.createElement('style');
  43. style.innerHTML = `
  44. .category-bar { display: flex; flex-wrap: nowrap; overflow-x: scroll !important; overflow-y: hidden !important; white-space: nowrap; width: 100%; }
  45. .category-bar::-webkit-scrollbar { width: 0 !important; height: 0 !important; }
  46. html, body { height: 100%; margin: 0; padding: 0; overflow: auto !important; }
  47. video, audio { overflow: hidden !important; }
  48. `;
  49. document.head.appendChild(style);
  50. categories.forEach((cat, index) => {
  51. const btn = document.createElement('button');
  52. btn.innerText = cat;
  53. btn.dataset.index = index;
  54. Object.assign(btn.style, { padding: '8px 12px', border: 'none', borderRadius: '20px', backgroundColor: index === 0 ? '#007aff' : '#f0f0f0', color: index === 0 ? '#fff' : '#000', cursor: 'pointer', fontSize: '14px', fontWeight: 'normal', whiteSpace: 'nowrap', minWidth: '70px', height: '30px', display: 'flex', justifyContent: 'center', alignItems: 'center' });
  55. btn.onclick = (e) => { activeCategoryIndex = parseInt(e.target.dataset.index); filterResources(cat); };
  56. categoryBar.appendChild(btn);
  57. });
  58. const resourceList = document.createElement('div');
  59. Object.assign(resourceList.style, { flex: 1, overflowY: 'auto', padding: '10px' });
  60. modal.appendChild(modalHeader);
  61. modal.appendChild(categoryBar);
  62. modal.appendChild(resourceList);
  63. document.body.appendChild(snifferBtn);
  64. document.body.appendChild(modal);
  65. snifferBtn.onclick = () => { updateResourceList(); modal.style.display = 'flex'; snifferBtn.style.display = 'none'; };
  66. let allResources = [];
  67. function updateResourceList() {
  68. allResources = [];
  69. document.querySelectorAll('a, source, link, video, audio').forEach(el => {
  70. const url = el.href || el.src;
  71. if (url) {
  72. let type = '其他';
  73. 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 = '视频';
  74. 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 = '音频';
  75. 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 = '图片';
  76. 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 = '扩展';
  77. 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 = '文档';
  78. allResources.push({ name: formatName(url), url, type });
  79. }
  80. });
  81. filterResources(categories[activeCategoryIndex]);
  82. }
  83. function filterResources(category) {
  84. resourceList.innerHTML = '';
  85. const filtered = category === '全部' ? allResources : allResources.filter(res => res.type === category);
  86. if (filtered.length === 0) { resourceList.innerHTML = `<p style="color:#666; text-align:center;">暂无资源可供下载</p>`; } else {
  87. filtered.forEach(res => {
  88. if (!res.name) return;
  89. const item = document.createElement('div');
  90. Object.assign(item.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px', fontWeight: 'normal' });
  91. const name = document.createElement('span');
  92. name.innerText = res.name;
  93. name.style.flex = '1';
  94. name.style.textAlign = 'left';
  95. name.style.color = '#000';
  96. name.style.fontWeight = 'normal';
  97. const downloadBtn = document.createElement('button');
  98. downloadBtn.innerText = '下载';
  99. Object.assign(downloadBtn.style, { padding: '6px 12px', border: 'none', borderRadius: '15px', backgroundColor: '#007aff', color: '#fff', cursor: 'pointer', fontSize: '14px', fontWeight: 'bold', width: '70px', height: '30px', display: 'flex', justifyContent: 'center', alignItems: 'center' });
  100. downloadBtn.onclick = () => downloadResource(res.url, res.name);
  101. item.appendChild(name);
  102. item.appendChild(downloadBtn);
  103. resourceList.appendChild(item);
  104. });
  105. }
  106. categoryBar.querySelectorAll('button').forEach((btn, index) => {
  107. btn.style.backgroundColor = index === activeCategoryIndex ? '#007aff' : '#f0f0f0';
  108. btn.style.color = index === activeCategoryIndex ? '#fff' : '#000';
  109. btn.style.fontSize = '14px';
  110. });
  111. setTimeout(() => { categoryBar.scrollLeft = categoryBar.children[activeCategoryIndex].offsetLeft - 20; }, 0);
  112. }
  113. function downloadResource(url, filename) {
  114. if (url.endsWith('.m3u8')) { downloadM3U8(url, filename); return; }
  115. fetch(url).then(response => { const contentType = response.headers.get('content-type') || ''; return response.blob().then(blob => ({ blob, contentType })); })
  116. .then(({ blob, contentType }) => {
  117. const a = document.createElement('a');
  118. a.href = URL.createObjectURL(blob);
  119. 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'];
  120. const extIndex = filename.lastIndexOf('.');
  121. let ext = extIndex !== -1 ? filename.slice(extIndex).toLowerCase() : '';
  122. const randomNum = Math.floor(100 + Math.random() * 900);
  123. if (!ext) { ext = '.txt'; filename += ext; }
  124. if (!filename.trim()) { filename = `downloaded_file${randomNum}.txt`; }
  125. if (forcedExtensions.includes(ext) || contentType.startsWith('text/')) { a.download = filename; } else { a.download = filename; }
  126. document.body.appendChild(a);
  127. a.click();
  128. document.body.removeChild(a);
  129. })
  130. .catch(() => alert('资源下载失败'));
  131. }
  132. function downloadM3U8(url, filename) {
  133. const xhr = new XMLHttpRequest();
  134. xhr.open('GET', url, true);
  135. xhr.responseType = 'blob';
  136. xhr.onload = () => {
  137. const contentType = xhr.getResponseHeader('Content-Type') || '';
  138. if (xhr.status === 200 && contentType.includes('application/vnd.apple.mpegurl')) {
  139. const a = document.createElement('a');
  140. a.href = URL.createObjectURL(xhr.response);
  141. a.download = filename.endsWith('.m3u8') ? filename : `${filename}.m3u8`;
  142. document.body.appendChild(a);
  143. a.click();
  144. document.body.removeChild(a);
  145. } else {
  146. alert('M3U8 文件下载失败');
  147. }
  148. };
  149. xhr.send();
  150. }
  151. function formatName(url) {
  152. const parts = url.split('/');
  153. return parts.length > 0 ? decodeURIComponent(parts[parts.length - 1]) : '';
  154. }
  155. })();