// ==UserScript== // @name Universal Video Downloader (Supports M3U8s) // @namespace http://tampermonkey.net/ // @version 11.1 // @description Production-ready video downloader. Sorts by size/quality. Beautiful "Media Selection" menu. // @author Minoa // @license MIT // @match *://*/* // @require https://cdn.jsdelivr.net/npm/m3u8-parser@4.7.1/dist/m3u8-parser.min.js // @require https://cdn.jsdelivr.net/npm/@warren-bank/ffmpeg@0.12.10-wasmbinary.3/dist/umd/ffmpeg.js // @resource classWorkerURL https://cdn.jsdelivr.net/npm/@warren-bank/ffmpeg@0.12.10-wasmbinary.3/dist/umd/258.ffmpeg.js // @resource coreURL https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js // @resource wasmURL https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.wasm // @grant GM_addStyle // @grant GM_getResourceURL // @grant GM_setValue // @grant GM_getValue // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/554755/Universal%20Video%20Downloader%20%28Supports%20M3U8s%29.user.js // @updateURL https://update.greasyfork.icu/scripts/554755/Universal%20Video%20Downloader%20%28Supports%20M3U8s%29.meta.js // ==/UserScript== (function() { 'use strict'; // ========================================== // CONFIGURATION // ========================================== let floatingButton = null; let hiddenToggle = null; // State let isHidden = GM_getValue('uvs_hidden', false); let pressStartTime = 0; // Data Stores const detectedUrls = new Set(); const allDetectedVideos = new Map(); const downloadedBlobs = new Map(); // Settings const CONCURRENCY = 3; const MAX_RETRIES = 3; const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isTwitter = location.hostname.includes('twitter.com') || location.hostname.includes('x.com'); // Theme const THEME = { bg: 'rgba(20, 20, 20, 0.75)', // Glass background modalBg: 'rgba(30, 30, 30, 0.85)', border: 'rgba(255, 255, 255, 0.1)', text: '#ffffff', subText: '#aaaaaa', accent: '#d4a373', success: '#4ade80', error: '#f87171', info: '#60a5fa' }; // ========================================== // STYLES // ========================================== GM_addStyle(` /* Main Button */ #uvs-container { position: fixed; top: 15px; left: 15px; width: 46px; height: 46px; z-index: 2147483647; isolation: isolate; pointer-events: auto; transition: opacity 0.3s; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #uvs-float { position: absolute; top: 3px; left: 3px; width: 40px; height: 40px; background: ${THEME.bg}; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: ${THEME.accent}; border: 1px solid ${THEME.border}; border-radius: 50%; font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.2s, background 0.2s; box-shadow: 0 4px 15px rgba(0,0,0,0.3); user-select: none; } #uvs-float:hover { transform: scale(1.05); background: rgba(50, 50, 50, 0.9); } #uvs-svg { position: absolute; top: 0; left: 0; pointer-events: none; transform: rotate(-90deg); } /* Stealth Toggle */ #uvs-hidden-toggle { position: fixed; top: 10px; right: 10px; width: 18px; height: 18px; border: 2px solid rgba(255, 255, 255, 0.4); background: rgba(0, 0, 0, 0.3); z-index: 2147483647; cursor: pointer; opacity: 0.5; transition: all 0.2s; border-radius: 4px; } #uvs-hidden-toggle:hover { opacity: 1; background: ${THEME.success}; border-color: #fff; transform: scale(1.1); } /* Notifications */ .uvs-notification { position: fixed; top: 75px; left: 15px; background: ${THEME.bg}; backdrop-filter: blur(12px); color: ${THEME.text}; padding: 10px 16px; border-radius: 12px; border: 1px solid ${THEME.border}; font-size: 13px; font-weight: 500; z-index: 2147483646; display: flex; align-items: center; gap: 10px; box-shadow: 0 8px 20px rgba(0,0,0,0.25); animation: uvs-slide-in 0.3s ease-out; } @keyframes uvs-slide-in { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } /* Modal Overlay */ #uvs-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(6px); z-index: 2147483645; display: flex; align-items: center; justify-content: center; opacity: 0; animation: uvs-fade-in 0.2s forwards; } @keyframes uvs-fade-in { to { opacity: 1; } } /* Modal Content */ #uvs-modal { background: ${THEME.modalBg}; border: 1px solid ${THEME.border}; border-radius: 16px; width: 550px; max-width: 90%; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 25px 50px rgba(0,0,0,0.5); transform: scale(0.95); animation: uvs-scale-in 0.2s forwards; } @keyframes uvs-scale-in { to { transform: scale(1); } } /* Header */ .uvs-header { padding: 16px 20px; border-bottom: 1px solid ${THEME.border}; display: flex; justify-content: space-between; align-items: center; } .uvs-title { font-size: 16px; font-weight: 600; color: ${THEME.text}; letter-spacing: 0.5px; } .uvs-close { background: transparent; border: none; color: ${THEME.subText}; font-size: 20px; cursor: pointer; padding: 4px; line-height: 1; transition: color 0.2s; } .uvs-close:hover { color: ${THEME.text}; } /* List */ .uvs-list { overflow-y: auto; padding: 0; margin: 0; } .uvs-item { padding: 16px 20px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: background 0.2s; } .uvs-item:hover { background: rgba(255,255,255,0.08); } .uvs-item:last-child { border-bottom: none; } /* Item Content */ .uvs-info { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; margin-right: 15px; } .uvs-filename { font-size: 14px; color: ${THEME.text}; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .uvs-meta { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } /* Badges */ .uvs-badge { font-size: 10px; padding: 3px 6px; border-radius: 4px; background: rgba(255,255,255,0.1); color: ${THEME.subText}; font-weight: 600; letter-spacing: 0.3px; } .uvs-badge.hd { background: rgba(96, 165, 250, 0.2); color: ${THEME.info}; } .uvs-badge.size { background: rgba(74, 222, 128, 0.2); color: ${THEME.success}; } .uvs-badge.fmt { background: rgba(212, 163, 115, 0.2); color: ${THEME.accent}; } /* Action Icon */ .uvs-action { width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,0.05); display: flex; align-items: center; justify-content: center; color: ${THEME.text}; font-size: 16px; transition: all 0.2s; } .uvs-item:hover .uvs-action { background: ${THEME.accent}; color: #000; } /* Twitter */ .uvs-tw-btn { display: flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 999px; cursor: pointer; color: rgb(113, 118, 123); transition: 0.2s; } .uvs-tw-btn:hover { background: rgba(212, 163, 115, 0.1); color: ${THEME.accent}; } .uvs-tw-btn svg { width: 20px; height: 20px; fill: currentColor; } `); // ========================================== // HELPERS // ========================================== const VIDEO_EXT = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv', '.m4v']; function sanitizeFilename(name) { return (name || 'video').replace(/[<>:"\/\\|?*\x00-\x1F]/g, '').replace(/\s+/g, '_').substring(0, 150); } function getFilenameFromUrl(url) { try { const pathname = new URL(url).pathname; const name = pathname.substring(pathname.lastIndexOf('/') + 1); return decodeURIComponent(name) || 'video.mp4'; } catch(e) { return 'video.mp4'; } } function resolveUrl(baseUrl, relativeUrl) { try { return new URL(relativeUrl, baseUrl).href; } catch (e) { return relativeUrl; } } function formatBytes(bytes, decimals = 1) { if (!bytes) return ''; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['B', '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]; } function formatDuration(seconds) { if (!seconds) return ''; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, '0')}`; } // ========================================== // DETECTION LOGIC // ========================================== function scanDOM() { const elements = document.querySelectorAll('video, audio, source'); elements.forEach(el => { const src = el.src || el.currentSrc; if (src && !src.startsWith('blob:') && !src.startsWith('data:')) { let width = 0, height = 0, duration = 0; if (el.tagName === 'VIDEO') { width = el.videoWidth; height = el.videoHeight; duration = el.duration; } else if (el.tagName === 'SOURCE' && el.parentElement?.tagName === 'VIDEO') { width = el.parentElement.videoWidth; height = el.parentElement.videoHeight; duration = el.parentElement.duration; } registerVideo({ type: 'direct', url: src, width, height, duration, source: 'DOM' }); } }); } function isM3U8(url) { return url && (url.includes('.m3u8') || url.includes('.m3u')); } function isVideoUrl(url) { if (!url || url.startsWith('blob:') || url.startsWith('data:')) return false; const clean = url.split('?')[0].toLowerCase(); return VIDEO_EXT.some(ext => clean.endsWith(ext)); } const originalFetch = window.fetch; window.fetch = async function(...args) { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url; const response = await originalFetch.apply(this, args); if (url) { if (isM3U8(url)) { const clone = response.clone(); clone.text().then(text => handleM3U8(url, text)).catch(() => {}); } else if (isVideoUrl(url)) { registerVideo({ type: 'direct', url: url, source: 'Network' }); } } return response; }; const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { this.addEventListener('load', function() { try { if (url && isVideoUrl(url)) registerVideo({ type: 'direct', url: url, source: 'XHR' }); if (url && (!this.responseType || this.responseType === 'text')) { if (isM3U8(url) || (this.responseText && this.responseText.trim().startsWith('#EXTM3U'))) { handleM3U8(url, this.responseText); } } } catch(e) {} }); return originalOpen.apply(this, arguments); }; // ========================================== // REGISTRY // ========================================== function registerVideo(data) { const fullUrl = resolveUrl(location.href, data.url); if (allDetectedVideos.has(fullUrl)) { const existing = allDetectedVideos.get(fullUrl); if (!existing.width && data.width) existing.width = data.width; if (!existing.height && data.height) existing.height = data.height; if (!existing.duration && data.duration) existing.duration = data.duration; return; } if (data.url.includes('preview') && data.url.includes('.jpg')) return; const videoObj = { url: fullUrl, type: data.type, filename: getFilenameFromUrl(fullUrl), width: data.width || 0, height: data.height || 0, duration: data.duration || 0, size: data.size || 0, // Estimated size in bytes timestamp: Date.now(), manifest: data.manifest || null }; allDetectedVideos.set(fullUrl, videoObj); if (!isHidden) notify(`✓ Video Detected`, 'success'); updateButtonState(); } function handleM3U8(url, content) { const fullUrl = resolveUrl(location.href, url); if (detectedUrls.has(fullUrl)) return; detectedUrls.add(fullUrl); try { const parser = new m3u8Parser.Parser(); parser.push(content); parser.end(); const manifest = parser.manifest; if (manifest.playlists && manifest.playlists.length > 0) return; let duration = 0; if (manifest.segments) manifest.segments.forEach(s => duration += s.duration); // Estimate Size: Duration (s) * Bandwidth (bits/s) / 8 // We look for a bandwidth attribute in the parent playlist usually, but here we might just have segments. // If we can't find it, we assume a decent bitrate (e.g., 2Mbps) for sorting purposes or leave as 0. let estimatedSize = 0; // Note: In a media playlist, bandwidth isn't usually explicitly stated unless we parsed the master. // We can try to guess from segment length if available, but for now let's prioritize duration. registerVideo({ type: 'm3u8', url: fullUrl, manifest: manifest, duration: duration, size: estimatedSize, source: 'M3U8' }); } catch(e) {} } // ========================================== // SORTING & UI // ========================================== function getSortedVideos() { return Array.from(allDetectedVideos.values()).sort((a, b) => { // 1. Sort by Size (if known) if (a.size > 0 || b.size > 0) return b.size - a.size; // 2. Sort by Resolution (Width * Height) const resA = (a.width || 0) * (a.height || 0); const resB = (b.width || 0) * (b.height || 0); if (resB !== resA) return resB - resA; // 3. Sort by Duration const durA = a.duration || 0; const durB = b.duration || 0; if (durB !== durA) return durB - durA; // 4. M3U8 Preference if (a.type === 'm3u8' && b.type !== 'm3u8') return -1; if (b.type === 'm3u8' && a.type !== 'm3u8') return 1; return b.timestamp - a.timestamp; }); } function createUI() { if (floatingButton) return; hiddenToggle = document.createElement('div'); hiddenToggle.id = 'uvs-hidden-toggle'; hiddenToggle.title = 'Show Video Downloader (Alt+Shift+V)'; if (!isHidden) hiddenToggle.style.display = 'none'; hiddenToggle.onclick = () => toggleStealthMode(false); document.body.appendChild(hiddenToggle); const container = document.createElement('div'); container.id = 'uvs-container'; if (isHidden) container.style.display = 'none'; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '46'); svg.setAttribute('height', '46'); svg.id = 'uvs-svg'; const track = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); track.setAttribute('cx', '23'); track.setAttribute('cy', '23'); track.setAttribute('r', '21'); track.setAttribute('fill', 'none'); track.setAttribute('stroke', THEME.success); track.setAttribute('stroke-width', '2'); track.setAttribute('stroke-opacity', '0.25'); const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', '23'); circle.setAttribute('cy', '23'); circle.setAttribute('r', '21'); circle.setAttribute('fill', 'none'); circle.setAttribute('stroke', THEME.success); circle.setAttribute('stroke-width', '2'); circle.setAttribute('stroke-dasharray', '132'); circle.setAttribute('stroke-dashoffset', '132'); circle.setAttribute('stroke-linecap', 'round'); svg.appendChild(track); svg.appendChild(circle); container.appendChild(svg); const btn = document.createElement('div'); btn.id = 'uvs-float'; btn.innerHTML = '⬇'; btn.progressCircle = circle; let pressInterval; const startPress = (e) => { if (e.button !== 0 && e.type !== 'touchstart') return; pressStartTime = Date.now(); pressInterval = setInterval(() => { const duration = Date.now() - pressStartTime; if (duration > 5000) btn.innerHTML = '👁️'; }, 100); }; const endPress = (e) => { clearInterval(pressInterval); const duration = Date.now() - pressStartTime; updateButtonState(); if (duration > 5000) toggleStealthMode(true); else if (duration < 500) handleClick(); }; btn.addEventListener('mousedown', startPress); btn.addEventListener('mouseup', endPress); btn.addEventListener('touchstart', startPress); btn.addEventListener('touchend', endPress); container.appendChild(btn); document.body.appendChild(container); floatingButton = btn; } function toggleStealthMode(hide) { if (hide === undefined) hide = !isHidden; isHidden = hide; GM_setValue('uvs_hidden', isHidden); const container = document.getElementById('uvs-container'); if (hiddenToggle) hiddenToggle.style.display = isHidden ? 'block' : 'none'; if (container) container.style.display = isHidden ? 'none' : 'block'; if (!isHidden) notify('Restored'); } window.addEventListener('keydown', (e) => { if (e.altKey && e.shiftKey && (e.key === 'V' || e.key === 'v')) toggleStealthMode(); }); function handleClick() { const videos = getSortedVideos(); if (videos.length === 0) notify('✕ No videos', 'error'); else if (videos.length === 1) processVideo(videos[0]); else showPopup(videos); } function updateButtonState() { if (allDetectedVideos.size > 0 && !floatingButton) createUI(); if (floatingButton) floatingButton.innerHTML = allDetectedVideos.size > 1 ? '☰' : '⬇'; } function updateProgress(percent) { if (!floatingButton) return; floatingButton.progressCircle.setAttribute('stroke-dashoffset', 132 - (132 * percent / 100)); } function notify(msg, type = 'info') { if (isHidden) return; const div = document.createElement('div'); div.className = 'uvs-notification'; let icon = 'ℹ'; if (type === 'success') { div.style.borderColor = THEME.success; div.style.color = THEME.success; icon = '✓'; } if (type === 'error') { div.style.borderColor = THEME.error; div.style.color = THEME.error; icon = '✕'; } div.innerHTML = `${icon} ${msg}`; document.body.appendChild(div); setTimeout(() => div.remove(), 3500); } function showPopup(videos) { document.getElementById('uvs-overlay')?.remove(); const overlay = document.createElement('div'); overlay.id = 'uvs-overlay'; const modal = document.createElement('div'); modal.id = 'uvs-modal'; // Header const header = document.createElement('div'); header.className = 'uvs-header'; header.innerHTML = ` Media Selection `; header.querySelector('.uvs-close').onclick = () => overlay.remove(); modal.appendChild(header); // List const list = document.createElement('div'); list.className = 'uvs-list'; videos.forEach(v => { const item = document.createElement('div'); item.className = 'uvs-item'; // Badges let badges = ''; if (v.type === 'm3u8') badges += `STREAM`; else badges += `MP4`; if (v.width && v.height) { const isHD = v.width >= 1280 || v.height >= 720; badges += `${v.width}x${v.height}`; } if (v.duration) badges += `${formatDuration(v.duration)}`; if (v.size) badges += `${formatBytes(v.size)}`; item.innerHTML = `