您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Finds all images/videos on a page (including CSS backgrounds and embedded media) and displays them in a feature-rich, draggable panel with advanced filtering, sorting, and settings.
// ==UserScript== // @name Advanced Media Analyzer // @namespace http://tampermonkey.net/ // @version 3.4 // @description Finds all images/videos on a page (including CSS backgrounds and embedded media) and displays them in a feature-rich, draggable panel with advanced filtering, sorting, and settings. // @author Gemini // @match *://*/* // @grant GM_addStyle // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net // ==/UserScript== (function() { 'use strict'; let allMediaData = []; // Holds all found media to allow re-sorting/filtering let originalElements = new Map(); // Maps a media src to its original DOM element for highlighting const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.ogv', '.m4v', '.mpeg']; // --- Settings Configuration --- let settings = { showToolbar: true, showBulkActions: true, enableHoverHighlight: true, scanCssBackgrounds: true }; const SETTINGS_KEY = 'mediaAnalyzerSettings'; function saveSettings() { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } function loadSettings() { const saved = localStorage.getItem(SETTINGS_KEY); if (saved) { settings = { ...settings, ...JSON.parse(saved) }; } } // --- End of Settings --- /** * Initializes and injects the floating "Analyze Media" button onto the page. */ function initFloatingButton() { const button = document.createElement('button'); button.id = 'media-sorter-trigger-button'; button.innerHTML = ` <svg style="width:18px;height:18px;margin-right:8px;" viewBox="0 0 24 24"> <path fill="currentColor" d="M10 12L12 14L14 12L10 12M10 8L12 10L14 8L10 8M10 16L12 18L14 16L10 16M21 3H3C1.9 3 1 3.9 1 5V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V5C23 3.9 22.1 3 21 3M21 19H3V5H21V19Z" /> </svg> Analyze Media `; button.addEventListener('click', createSortedMediaPanel); document.body.appendChild(button); } /** * Formats bytes into a human-readable string. */ function formatBytes(bytes, decimals = 2) { if (!bytes || bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } /** * Parses a file size string (e.g., "1.5 MB") into bytes. */ function parseSizeString(sizeStr) { if (!sizeStr) return null; const cleanedStr = sizeStr.trim().toUpperCase(); const parts = cleanedStr.match(/^([\d.]+)\s*([KMGT]?B)$/); if (!parts) return null; const value = parseFloat(parts[1]); const unit = parts[2]; let multiplier = 1; switch (unit) { case 'KB': multiplier = 1024; break; case 'MB': multiplier = 1024 ** 2; break; case 'GB': multiplier = 1024 ** 3; break; case 'TB': multiplier = 1024 ** 4; break; } return Math.round(value * multiplier); } /** * Determines if a media item is an image or video based on its URL. * This is more robust as it ignores the element type (e.g. an <img> for a video thumbnail). */ function getMediaType(src) { try { // Use URL object to ignore query strings/fragments when checking extension const lowerPath = new URL(src, document.baseURI).pathname.toLowerCase(); for (const ext of VIDEO_EXTENSIONS) { if (lowerPath.endsWith(ext)) return 'video'; } } catch (e) { // Fallback for data URIs or invalid URLs const lowerSrc = src.toLowerCase(); for (const ext of VIDEO_EXTENSIONS) { if (lowerSrc.includes(ext)) return 'video'; } } return 'image'; } /** * Builds the main UI panel and initiates the media scan. */ function createSortedMediaPanel() { if (document.getElementById('media-sorter-panel')) { document.getElementById('media-sorter-panel').remove(); } const panel = document.createElement('div'); panel.id = 'media-sorter-panel'; panel.innerHTML = ` <div class="media-sorter-header"> <h2 id="media-sorter-title">Analyzing Media...</h2> <div> <button id="media-sorter-settings-btn" class="header-icon-btn" title="Settings"> <svg viewBox="0 0 24 24" style="width:20px;height:20px;"><path fill="currentColor" d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.25C8.66,5.49,8.13,5.81,7.63,6.19L5.24,5.23C5.02,5.16,4.77,5.23,4.65,5.45L2.73,8.77 c-0.11,0.2-0.06,0.47,0.12,0.61l2.03,1.58C4.82,11.36,4.8,11.68,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.44 c0.04,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.44c0.59-0.24,1.12-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0.01,0.59-0.22l1.92-3.32c0.11-0.2,0.06-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg> </button> <button id="media-sorter-close" class="header-icon-btn" title="Close panel">×</button> </div> </div> <div class="media-sorter-toolbar" ${settings.showToolbar ? '' : 'style="display:none;"'}> <select id="media-filter-type"> <option value="all">All Media</option> <option value="image">Images</option> <option value="video">Videos</option> </select> <select id="media-sort-by"> <option value="size_desc">Sort by Size (Largest)</option> <option value="size_asc">Sort by Size (Smallest)</option> <option value="url_asc">Sort by URL (A-Z)</option> </select> </div> <div id="media-sorter-list"><div class="media-sorter-loader"></div></div> <div class="media-sorter-footer" ${settings.showBulkActions ? '' : 'style="display:none;"'}> <button id="copy-all-urls">Copy All URLs</button> <button id="download-all-urls">Download URLs as .txt</button> </div> `; document.body.appendChild(panel); makeDraggable(panel); panel.querySelector('#media-sorter-close').addEventListener('click', () => panel.remove()); panel.querySelector('#media-sorter-settings-btn').addEventListener('click', createSettingsPanel); panel.querySelector('#media-filter-type').addEventListener('change', renderMediaList); panel.querySelector('#media-sort-by').addEventListener('change', renderMediaList); panel.querySelector('#copy-all-urls').addEventListener('click', copyAllUrls); panel.querySelector('#download-all-urls').addEventListener('click', downloadAllUrls); setTimeout(scanAndPopulateMedia, 100); } /** * Builds and displays the settings panel. */ function createSettingsPanel() { if (document.getElementById('media-sorter-settings-panel')) return; const settingsPanel = document.createElement('div'); settingsPanel.id = 'media-sorter-settings-panel'; settingsPanel.innerHTML = ` <div class="media-settings-content"> <h3>Settings</h3> <label><input type="checkbox" data-setting="showToolbar" ${settings.showToolbar ? 'checked' : ''}> Show Filter/Sort Toolbar</label> <label><input type="checkbox" data-setting="showBulkActions" ${settings.showBulkActions ? 'checked' : ''}> Show Bulk Actions Footer</label> <label><input type="checkbox" data-setting="enableHoverHighlight" ${settings.enableHoverHighlight ? 'checked' : ''}> Enable Hover to Highlight</label> <label><input type="checkbox" data-setting="scanCssBackgrounds" ${settings.scanCssBackgrounds ? 'checked' : ''}> Scan for CSS Background Images</label> <button id="settings-close-btn">Close</button> </div> `; document.getElementById('media-sorter-panel').appendChild(settingsPanel); settingsPanel.querySelector('#settings-close-btn').addEventListener('click', () => settingsPanel.remove()); settingsPanel.addEventListener('change', e => { if (e.target.type === 'checkbox') { const key = e.target.dataset.setting; const value = e.target.checked; settings[key] = value; saveSettings(); // Dynamically update UI based on new setting const mainPanel = document.getElementById('media-sorter-panel'); if (key === 'showToolbar') mainPanel.querySelector('.media-sorter-toolbar').style.display = value ? 'flex' : 'none'; if (key === 'showBulkActions') mainPanel.querySelector('.media-sorter-footer').style.display = value ? 'flex' : 'none'; if (key === 'scanCssBackgrounds') { // Offer to rescan e.target.closest('label').innerHTML += ' <small>(<a href="#" id="rescan-link">Rescan page</a>)</small>'; document.getElementById('rescan-link').onclick = (evt) => { evt.preventDefault(); mainPanel.querySelector('#media-sorter-list').innerHTML = '<div class="media-sorter-loader"></div>'; setTimeout(scanAndPopulateMedia, 100); settingsPanel.remove(); }; } renderMediaList(); } }); } /** * Scans the page for all types of media and then triggers the initial render. */ function scanAndPopulateMedia() { allMediaData = []; originalElements.clear(); const processedSources = new Set(); const resourceMap = new Map(performance.getEntriesByType('resource').map(res => [res.name, res.encodedBodySize])); scanAttachmentLists(processedSources); scanHtmlTags(processedSources, resourceMap); if (settings.scanCssBackgrounds) { scanCssBackgrounds(processedSources, resourceMap); } renderMediaList(); } /** * Renders the list of media items based on current filter and sort settings. */ function renderMediaList() { const panel = document.getElementById('media-sorter-panel'); if (!panel) return; const listContainer = panel.querySelector('#media-sorter-list'); const titleElement = panel.querySelector('#media-sorter-title'); const filterValue = panel.querySelector('#media-filter-type').value; const sortValue = panel.querySelector('#media-sort-by').value; let filteredData = allMediaData; if (filterValue !== 'all') { filteredData = allMediaData.filter(item => item.type === filterValue); } switch (sortValue) { case 'size_desc': filteredData.sort((a, b) => b.size - a.size); break; case 'size_asc': filteredData.sort((a, b) => a.size - b.size); break; case 'url_asc': filteredData.sort((a, b) => a.src.localeCompare(b.src)); break; } titleElement.textContent = `Media Analyzer (${filteredData.length} items)`; listContainer.innerHTML = ''; if (filteredData.length > 0) { filteredData.forEach(item => { const itemDiv = document.createElement('div'); itemDiv.className = 'media-sorter-item'; itemDiv.dataset.src = item.src; const preview = item.previewElement.cloneNode(true); preview.style.width = '100px'; preview.style.height = '100px'; preview.style.objectFit = 'contain'; if (preview.tagName === 'VIDEO') { preview.muted = true; } const infoDiv = document.createElement('div'); infoDiv.className = 'media-sorter-info'; infoDiv.innerHTML = ` <p><strong>Size:</strong> ${formatBytes(item.size)}</p> <div class="media-source-container"> <p><strong>Source:</strong> <a href="${item.src}" target="_blank" rel="noopener noreferrer" title="${item.src}">${item.src}</a></p> ${item.src !== 'Embedded Data URI' ? '<button class="copy-src-button" title="Copy URL">Copy</button>' : ''} </div> `; itemDiv.appendChild(preview); itemDiv.appendChild(infoDiv); listContainer.appendChild(itemDiv); if (settings.enableHoverHighlight && originalElements.has(item.src)) { itemDiv.addEventListener('mouseenter', () => addHighlight(item.src)); itemDiv.addEventListener('mouseleave', () => removeHighlight(item.src)); } }); } else { listContainer.innerHTML = '<p class="media-sorter-empty">No media matching the current filter was found.</p>'; } panel.querySelectorAll('.copy-src-button').forEach(button => { button.addEventListener('click', e => { const url = e.target.closest('.media-source-container').querySelector('a').href; navigator.clipboard.writeText(url).then(() => { e.target.textContent = 'Copied!'; setTimeout(() => { e.target.textContent = 'Copy'; }, 2000); }); }); }); } /** * Scans for media within custom attachment list structures. */ function scanAttachmentLists(processedSources) { document.querySelectorAll('.attachmentList > li, .attachmentList-item').forEach(item => { const metaEl = item.querySelector('.file-meta'); const previewLink = item.querySelector('a.file-preview[href]'); // This is the essential check. We need a link and file metadata. if (previewLink && metaEl) { const src = new URL(previewLink.getAttribute('href'), document.baseURI).href; if (src && !processedSources.has(src)) { const size = parseSizeString(metaEl.innerText); if (size !== null) { let previewElement; const thumbnailEl = item.querySelector('img'); const mediaType = getMediaType(src); const elementToHighlight = previewLink; // The link is always the best element to highlight if (thumbnailEl) { // If there's a thumbnail, use it for the preview. previewElement = thumbnailEl; } else if (mediaType === 'video') { // If no thumbnail but it's a video, create a video element for the preview. const videoPreview = document.createElement('video'); videoPreview.src = src; previewElement = videoPreview; } else { // Fallback for non-video items without a thumbnail (e.g., zip files). // We can skip these as this tool focuses on visual media. return; } allMediaData.push({ previewElement: previewElement, src: src, size: size, type: mediaType }); originalElements.set(src, elementToHighlight); processedSources.add(src); } } } }); } /** * Scans for standard <img> and embedded <video> tags on the page. */ function scanHtmlTags(processedSources, resourceMap) { // Find all images not already processed in attachment lists document.querySelectorAll('img').forEach(el => { const src = el.currentSrc || el.src; if (src && !processedSources.has(src)) { let size = 0; let finalSrc = src; if (src.startsWith('data:')) { const base64Data = src.split(',')[1]; if (base64Data) size = base64Data.length * 0.75; finalSrc = 'Embedded Data URI'; } else if (resourceMap.has(src)) { size = resourceMap.get(src); } if (size > 0) { allMediaData.push({ previewElement: el, src: finalSrc, size, type: getMediaType(finalSrc) }); if (finalSrc !== 'Embedded Data URI') { originalElements.set(finalSrc, el); } } processedSources.add(src); } }); // Find all videos, which might be embedded differently document.querySelectorAll('video').forEach(el => { const sourceEl = el.querySelector('source'); const src = el.src || (sourceEl ? sourceEl.src : null); const fullSrc = src ? new URL(src, document.baseURI).href : null; if (fullSrc && !processedSources.has(fullSrc)) { const size = resourceMap.get(fullSrc) || 0; // We list videos even if size isn't in resourceMap, as they are important allMediaData.push({ previewElement: el, src: fullSrc, size, type: 'video' }); originalElements.set(fullSrc, el); // Highlight the video player itself processedSources.add(fullSrc); } }); } /** * Scans CSS rules for background-image properties. */ function scanCssBackgrounds(processedSources, resourceMap) { const urlRegex = /url\(['"]?(.*?)['"]?\)/; for (const sheet of document.styleSheets) { try { if (sheet.cssRules) { for (const rule of sheet.cssRules) { if (rule.style && rule.style.backgroundImage && rule.style.backgroundImage.includes('url')) { const matches = rule.style.backgroundImage.matchAll(new RegExp(urlRegex.source, 'g')); for (const match of matches) { if (match && match[1]) { const url = new URL(match[1], document.baseURI).href; if (url && !processedSources.has(url) && resourceMap.has(url)) { const size = resourceMap.get(url); if (size > 0) { const img = new Image(); img.src = url; allMediaData.push({ previewElement: img, src: url, size, type: getMediaType(url) }); processedSources.add(url); } } } } } } } } catch (e) { // Ignore cross-origin stylesheet errors } } } /** * Highlights the corresponding element on the page. */ function addHighlight(src) { const el = originalElements.get(src); if (el && typeof el.getBoundingClientRect === 'function') { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.classList.add('media-sorter-highlight'); } } /** * Removes the highlight from the element on the page. */ function removeHighlight(src) { const el = originalElements.get(src); if (el && typeof el.getBoundingClientRect === 'function') { el.classList.remove('media-sorter-highlight'); } } /** * Copies all currently filtered URLs to the clipboard. */ function copyAllUrls() { const panel = document.getElementById('media-sorter-panel'); const button = document.getElementById('copy-all-urls'); if (!panel || !button) return; const urls = Array.from(panel.querySelectorAll('.media-sorter-item a')) .map(a => a.href) .join('\n'); if (urls) { navigator.clipboard.writeText(urls).then(() => { button.textContent = 'Copied!'; setTimeout(() => { button.textContent = 'Copy All URLs'; }, 2000); }); } } /** * Downloads all currently filtered URLs as a text file. */ function downloadAllUrls() { const panel = document.getElementById('media-sorter-panel'); if (!panel) return; const urls = Array.from(panel.querySelectorAll('.media-sorter-item a')) .map(a => a.href) .join('\n'); if (urls) { const blob = new Blob([urls], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `media_urls_${document.domain}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } } /** * Makes the panel draggable. */ function makeDraggable(panel) { const header = panel.querySelector('.media-sorter-header'); let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; header.onmousedown = (e) => { // prevent dragging when clicking on a button if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; }; function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; panel.style.top = (panel.offsetTop - pos2) + "px"; panel.style.left = (panel.offsetLeft - pos1) + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } /** * Injects all CSS styles. */ function addPanelStyles() { GM_addStyle(` #media-sorter-trigger-button { position: fixed; bottom: 20px; right: 20px; z-index: 999998; background-color: #007bff; color: white; border: none; border-radius: 8px; padding: 10px 15px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15); transition: all 0.2s ease-in-out; display: flex; align-items: center; } #media-sorter-trigger-button:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.2); background-color: #0069d9; } #media-sorter-panel { position: fixed; top: 40px; right: 40px; width: 600px; max-width: 90vw; height: 85vh; background-color: rgba(250, 250, 250, 0.95); border: 1px solid #ccc; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.2); z-index: 999999; display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: #333; backdrop-filter: blur(10px); resize: both; overflow: hidden; } .media-sorter-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-bottom: 1px solid #ddd; cursor: move; user-select: none; background-color: rgba(255,255,255,0.7); } #media-sorter-title { margin: 0; font-size: 18px; font-weight: 600; flex-grow: 1; } .header-icon-btn { background: none; border: none; font-size: 28px; cursor: pointer; color: #888; padding: 0 5px; } .header-icon-btn:hover { color: #000; } #media-sorter-settings-btn svg { vertical-align: middle; } .media-sorter-toolbar { padding: 10px; display: flex; gap: 10px; border-bottom: 1px solid #ddd; background: #f8f9fa; } .media-sorter-toolbar select { padding: 8px; border: 1px solid #ccc; border-radius: 5px; flex-grow: 1; } #media-sorter-list { overflow-y: auto; padding: 10px; flex-grow: 1; } .media-sorter-item { display: flex; align-items: flex-start; border-bottom: 1px solid #eee; padding: 15px 10px; transition: background-color 0.2s; } .media-sorter-item:hover { background-color: rgba(0, 123, 255, 0.05); } .media-sorter-info { margin-left: 15px; flex-grow: 1; min-width: 0; } .media-sorter-info p { margin: 0 0 8px 0; font-size: 13px; word-break: break-all; } .media-sorter-info strong { color: #000; } .media-source-container { display: flex; align-items: center; justify-content: space-between; } .media-source-container a { color: #007bff; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .media-source-container a:hover { text-decoration: underline; } .copy-src-button { margin-left: 10px; padding: 3px 8px; font-size: 12px; border: 1px solid #ccc; background-color: #f0f0f0; border-radius: 5px; cursor: pointer; } .copy-src-button:hover { background-color: #e0e0e0; } .media-sorter-footer { padding: 10px; display: flex; gap: 10px; border-top: 1px solid #ddd; background: #f8f9fa; } .media-sorter-footer button { padding: 8px 12px; border: 1px solid #007bff; background-color: transparent; color: #007bff; border-radius: 5px; cursor: pointer; transition: all 0.2s; flex-grow: 1; } .media-sorter-footer button:hover { background-color: #007bff; color: white; } .media-sorter-empty, .media-sorter-loader { text-align: center; color: #777; margin-top: 40px; } .media-sorter-loader { border: 5px solid #f3f3f3; border-top: 5px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 40px auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .media-sorter-highlight { outline: 3px solid #007bff !important; box-shadow: 0 0 20px rgba(0, 123, 255, 0.8) !important; transition: outline 0.2s ease, box-shadow 0.2s ease; } #media-sorter-settings-panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(250, 250, 250, 0.98); z-index: 10; display: flex; align-items: center; justify-content: center; } .media-settings-content { background-color: white; padding: 25px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); display: flex; flex-direction: column; gap: 15px; } .media-settings-content h3 { margin: 0 0 10px; } .media-settings-content label { display: block; font-size: 14px; cursor: pointer; } .media-settings-content input { margin-right: 8px; } #settings-close-btn { margin-top: 10px; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; } `); } // --- Script Initialization --- loadSettings(); initFloatingButton(); addPanelStyles(); })();