// ==UserScript== // @name 资源嗅探 // @namespace http://tampermonkey.net/ // @version v4.2.12 // @description 自动嗅探网页图片/视频/音频/SVG资源,含源码查看、可视化编辑、SEO检测。移动端适配。 // @author 增强版 // @match *://*/* // @grant GM_addStyle // @grant GM_openInTab // @run-at document-start // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23ff6b6b%22%2F%3E%3Cstop%20offset%3D%220.5%22%20stop-color%3D%22%23feca57%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%231dd1a1%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Crect%20x%3D%223%22%20y%3D%223%22%20width%3D%2218%22%20height%3D%2218%22%20rx%3D%223%22%20fill%3D%22url(%23g)%22%2F%3E%3Ccircle%20cx%3D%228.5%22%20cy%3D%228.5%22%20r%3D%221.6%22%20fill%3D%22%23fff%22%2F%3E%3Cpath%20d%3D%22M21%2015l-5-5L7%2019%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M12%2017v-4%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%2F%3E%3Cpath%20d%3D%22M9.5%2013L12%2010.5L14.5%2013%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E // @license MIT // ==/UserScript== (function () { 'use strict'; // ============================================================ // 1. 存储层 // ============================================================ const allResources = { video: [], audio: [], image: [], other: [] }; // ============================================================ // 2. 嗅探引擎 // ============================================================ if (location.protocol === 'chrome:' || location.protocol === 'edge:' || location.hostname === '') return; const resourceTypes = { video: new Set(['.mp4', '.flv', '.m3u8', '.avi', '.wmv', '.mov']), audio: new Set(['.mp3', '.wav', '.ogg', '.m4a']), image: new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']) }; // 常规扩展名检查 const imageExtSet = new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.avif', '.tiff', '.tif']); const videoExtSet = new Set(['.mp4', '.flv', '.m3u8', '.avi', '.wmv', '.mov', '.webm', '.mkv', '.ts', '.mpeg']); const audioExtSet = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac', '.wma']); function categorizeUrl(url) { if (!url || typeof url !== 'string') return; // 跳过空字符串、纯数字ID(无法直接访问的资源标记) if (url.trim() === '') return; try { const urlObj = new URL(url); let type = 'other'; if (urlObj.protocol === 'data:') { const mime = urlObj.pathname.split(';')[0]; if (mime.startsWith('image/')) type = 'image'; else if (mime.startsWith('video/')) type = 'video'; else if (mime.startsWith('audio/')) type = 'audio'; addResource(type, url); return; } // 检查文件扩展名 const path = urlObj.pathname; const dotIdx = path.lastIndexOf('.'); if (dotIdx !== -1) { const ext = path.substring(dotIdx).toLowerCase().split('?')[0].split('#')[0]; if (imageExtSet.has(ext)) type = 'image'; else if (videoExtSet.has(ext)) type = 'video'; else if (audioExtSet.has(ext)) type = 'audio'; } else { // 无扩展名:检查是否包含图片服务常见关键词 const lowerUrl = url.toLowerCase(); if (lowerUrl.includes('/image/') || lowerUrl.includes('/img/') || lowerUrl.includes('/photo/') || lowerUrl.includes('/thumbnail/') || lowerUrl.includes('/thumb/') || lowerUrl.includes('/picture/') || lowerUrl.includes('image') || lowerUrl.includes('img') || lowerUrl.startsWith('//') && (lowerUrl.includes('.webp') || lowerUrl.includes('.jpg') || lowerUrl.includes('.png'))) { type = 'image'; } } addResource(type, url); } catch (e) { /* 忽略 */ } } // 智能检查一个URL是否为图片(通过尝试加载) function isImageUrl(url) { if (!url || typeof url !== 'string') return false; // 先通过扩展名快速判断 try { const urlObj = new URL(url); const path = urlObj.pathname; const dotIdx = path.lastIndexOf('.'); if (dotIdx !== -1) { const ext = path.substring(dotIdx).toLowerCase().split('?')[0].split('#')[0]; if (imageExtSet.has(ext)) return true; } // data URL if (url.startsWith('data:image/')) return true; // 包含图片特征但不一定是 if (url.startsWith('blob:')) return true; } catch(e) {} return false; } function addResource(type, url) { if (!url || allResources[type].includes(url)) return; allResources[type].push(url); if (window._hyUIReady) window._hyAddResourceItem(type, url); } // 常见懒加载图片属性列表 const lazyAttrs = ['data-src', 'data-original', 'data-lazy-src', 'data-srcset', 'data-url', 'data-echo', 'data-lazy', 'data-full', 'data-real-src', 'data-bg', 'data-bg-url', 'data-image', 'data-img', 'data-load', 'data-lazyload', 'data-original-src', 'data-highres', 'data-normal', 'data-small', 'data-medium', 'data-large']; function scanDOM() { // 1. 标准标签的 src / srcset ['video', 'audio', 'img', 'image'].forEach(tag => { document.querySelectorAll(tag).forEach(el => { if (el.closest && el.closest('#_hy-root')) return; // 排除脚本自身UI if (el.src) categorizeUrl(el.src); if (el.srcset) { el.srcset.split(',').forEach(s => { const url = s.trim().split(' ')[0]; categorizeUrl(url); }); } // 懒加载属性 lazyAttrs.forEach(attr => { const val = el.getAttribute(attr); if (val) categorizeUrl(val); }); }); }); // 2. 单独扫描所有元素的懒加载属性(用于那些非标准标签或自定义元素) const lazySelector = lazyAttrs.map(a => '[' + a + ']').join(','); if (lazySelector) { document.querySelectorAll(lazySelector).forEach(el => { if (el.closest && el.closest('#_hy-root')) return; // 排除脚本自身UI lazyAttrs.forEach(attr => { const val = el.getAttribute(attr); if (val) categorizeUrl(val); }); }); } // 3. 内联 SVG document.querySelectorAll('svg').forEach(svg => { if (svg.closest && svg.closest('#_hy-root')) return; // 排除脚本自身UI if (!svg.querySelector('*') && (!svg.textContent || !svg.textContent.trim())) return; if (svg.closest('img')) return; try { const clone = svg.cloneNode(true); if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); const svgStr = new XMLSerializer().serializeToString(clone); if (svgStr.length < 50) return; addResource('image', 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr)); } catch (e) { /* 忽略 */ } }); // 4. object/embed SVG document.querySelectorAll('object[type="image/svg+xml"], object[data$=".svg"], embed[type="image/svg+xml"], embed[src$=".svg"]').forEach(el => { if (el.closest && el.closest('#_hy-root')) return; const url = el.data || el.src; if (url) categorizeUrl(url); }); // 5. /