// ==UserScript== // @name Universal Video Downloader (Stealth + Smart Save) // @namespace http://tampermonkey.net/ // @version 9.0 // @description Downloads streams. Features: Stealth mode, Smart Desktop/Mobile handling, Concurrent downloading. // @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 none // ==/UserScript== (function() { 'use strict'; // ========================================== // CONFIGURATION // ========================================== let floatingButton = null; let hiddenToggle = null; let debugConsole = null; // State let isHidden = GM_getValue('uvs_hidden', false); let isDebug = GM_getValue('uvs_debug', false); let pressTimer = null; let pressStartTime = 0; // Download Settings const CONCURRENCY = 3; const MAX_RETRIES = 3; const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // Data Stores const detectedM3U8Urls = new Set(); const allDetectedVideos = new Map(); const downloadedBlobs = new Map(); // FFmpeg let ffmpegInstance = null; let ffmpegLoaded = false; let wasmBinaryCache = null; const isTwitter = location.hostname.includes('twitter.com') || location.hostname.includes('x.com'); // Theme const THEME = { bg: 'rgba(20, 20, 20, 0.6)', border: 'rgba(255, 255, 255, 0.1)', text: '#ffffff', accent: '#d4a373', success: '#4ade80', error: '#f87171' }; // ========================================== // 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; } #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(40, 40, 40, 0.8); } #uvs-svg { position: absolute; top: 0; left: 0; pointer-events: none; transform: rotate(-90deg); } /* Stealth Toggle (Tiny Checkbox) */ #uvs-hidden-toggle { position: fixed; top: 5px; right: 5px; width: 12px; height: 12px; border: 1px solid rgba(255,255,255,0.3); background: rgba(0,0,0,0.1); z-index: 2147483647; cursor: pointer; opacity: 0.1; transition: opacity 0.2s, background 0.2s; border-radius: 2px; } #uvs-hidden-toggle:hover { opacity: 0.8; background: rgba(255,255,255,0.1); } #uvs-hidden-toggle.checked { background: ${THEME.success}; opacity: 0.6; } /* 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-family: sans-serif; 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); } } /* Popup */ #uvs-popup { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); z-index: 2147483645; display: flex; align-items: center; justify-content: center; } #uvs-popup-content { background: rgba(30, 30, 30, 0.85); backdrop-filter: blur(16px); border: 1px solid ${THEME.border}; border-radius: 16px; width: 400px; max-width: 90%; max-height: 80vh; overflow-y: auto; color: #eee; font-family: sans-serif; box-shadow: 0 20px 40px rgba(0,0,0,0.4); } .uvs-item { padding: 14px 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.1); } .uvs-tag { font-size: 10px; padding: 3px 8px; border-radius: 6px; background: rgba(255,255,255,0.1); margin-right: 10px; color: #ccc; } /* Debug Console */ #uvs-debug { position: fixed; bottom: 0; left: 0; width: 100%; height: 150px; background: rgba(0,0,0,0.9); color: #0f0; font-family: monospace; font-size: 11px; padding: 10px; overflow-y: auto; z-index: 2147483647; display: none; border-top: 1px solid #333; } /* 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 resolveUrl(baseUrl, relativeUrl) { try { return new URL(relativeUrl, baseUrl).href; } catch (e) { return relativeUrl; } } function log(msg) { if (isDebug) { console.log('[UVS]', msg); if (debugConsole) { const line = document.createElement('div'); line.textContent = `> ${msg}`; debugConsole.appendChild(line); debugConsole.scrollTop = debugConsole.scrollHeight; } } } // ========================================== // FFMPEG ENGINE // ========================================== async function initFFmpeg() { if (ffmpegLoaded && ffmpegInstance) return ffmpegInstance; notify('⟳ Loading Engine...'); try { if (!wasmBinaryCache) { const wasmURL = GM_getResourceURL('wasmURL', false); const resp = await fetch(wasmURL); wasmBinaryCache = await resp.arrayBuffer(); } ffmpegInstance = new window.FFmpegWASM.FFmpeg(); ffmpegInstance.on('progress', ({ progress }) => updateProgress(Math.round(progress * 100))); await ffmpegInstance.load({ classWorkerURL: GM_getResourceURL('classWorkerURL', false), coreURL: GM_getResourceURL('coreURL', false), wasmBinary: wasmBinaryCache, }); ffmpegLoaded = true; notify('✓ Engine Ready', 'success'); return ffmpegInstance; } catch(e) { notify('✕ Engine Failed', 'error'); throw e; } } async function convertToMP4(tsBlob, filename) { const ffmpeg = await initFFmpeg(); const inputName = 'input.ts'; const outputName = filename.endsWith('.mp4') ? filename : filename + '.mp4'; notify('⟳ Converting...'); try { await ffmpeg.writeFile(inputName, new Uint8Array(await tsBlob.arrayBuffer())); await ffmpeg.exec(['-i', inputName, '-c', 'copy', '-movflags', 'faststart', outputName]); const data = await ffmpeg.readFile(outputName); await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); return new Blob([data.buffer], { type: 'video/mp4' }); } catch(e) { notify('✕ Conversion Failed', 'error'); throw e; } } // ========================================== // NETWORK INTERCEPTOR // ========================================== function isM3U8(url) { return url && (url.includes('.m3u8') || url.includes('.m3u')); } function isVideoUrl(url) { if (!url || url.startsWith('blob:')) 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 && isM3U8(url)) { const clone = response.clone(); clone.text().then(text => handleM3U8Detection(url, text)).catch(() => {}); } else if (url && isVideoUrl(url)) { handleVideoDetection(url); } return response; }; const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { this.addEventListener('load', function() { try { if (url && isVideoUrl(url)) handleVideoDetection(url); if (url && (!this.responseType || this.responseType === 'text')) { if (isM3U8(url) || (this.responseText && this.responseText.trim().startsWith('#EXTM3U'))) { handleM3U8Detection(url, this.responseText); } } } catch(e) {} }); return originalOpen.apply(this, arguments); }; function handleVideoDetection(url) { const fullUrl = resolveUrl(location.href, url); if (!allDetectedVideos.has(fullUrl)) { log(`Video detected: ${fullUrl}`); allDetectedVideos.set(fullUrl, { type: 'direct', url: fullUrl, title: fullUrl.split('/').pop().split('?')[0], timestamp: Date.now() }); updateButtonState(); } } function handleM3U8Detection(url, content) { const fullUrl = resolveUrl(location.href, url); if (detectedM3U8Urls.has(fullUrl)) return; detectedM3U8Urls.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; if (manifest.segments && manifest.segments.length > 0) { let duration = 0; manifest.segments.forEach(s => duration += s.duration); log(`Stream detected: ${fullUrl} (${duration}s)`); allDetectedVideos.set(fullUrl, { type: 'm3u8', url: fullUrl, manifest: manifest, duration: duration, title: `Stream (${Math.ceil(duration)}s)`, timestamp: Date.now() }); notify(`✓ Stream Detected (${Math.ceil(duration)}s)`, 'success'); updateButtonState(); } } catch(e) { log('Error parsing M3U8'); } } // ========================================== // UI & INTERACTION // ========================================== function createUI() { if (floatingButton) return; // 1. Hidden Toggle (Top Right) hiddenToggle = document.createElement('div'); hiddenToggle.id = 'uvs-hidden-toggle'; hiddenToggle.title = 'Show Video Downloader'; if (!isHidden) hiddenToggle.style.display = 'none'; hiddenToggle.onclick = () => { isHidden = false; GM_setValue('uvs_hidden', false); hiddenToggle.style.display = 'none'; document.getElementById('uvs-container').style.display = 'block'; notify('Button Restored'); }; document.body.appendChild(hiddenToggle); // 2. Debug Console debugConsole = document.createElement('div'); debugConsole.id = 'uvs-debug'; if (isDebug) debugConsole.style.display = 'block'; document.body.appendChild(debugConsole); // 3. Main Button 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; // Long Press Logic let pressInterval; const startPress = (e) => { if (e.button !== 0 && e.type !== 'touchstart') return; // Only left click pressStartTime = Date.now(); pressInterval = setInterval(() => { const duration = Date.now() - pressStartTime; if (duration > 10000) btn.innerHTML = '🪲'; // Bug else if (duration > 5000) btn.innerHTML = '👁️'; // Eye }, 100); }; const endPress = (e) => { clearInterval(pressInterval); const duration = Date.now() - pressStartTime; // Reset Icon updateButtonState(); if (duration > 10000) { // Toggle Debug isDebug = !isDebug; GM_setValue('uvs_debug', isDebug); debugConsole.style.display = isDebug ? 'block' : 'none'; notify(`Debug Mode: ${isDebug ? 'ON' : 'OFF'}`); } else if (duration > 5000) { // Hide Button isHidden = true; GM_setValue('uvs_hidden', true); container.style.display = 'none'; hiddenToggle.style.display = 'block'; notify('Button Hidden (Check top-right)'); } else if (duration < 500) { // Normal Click 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 handleClick() { const videos = Array.from(allDetectedVideos.values()).sort((a, b) => b.timestamp - a.timestamp); 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') { 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-popup')?.remove(); const overlay = document.createElement('div'); overlay.id = 'uvs-popup'; const content = document.createElement('div'); content.id = 'uvs-popup-content'; const header = document.createElement('div'); header.style.padding = '15px 20px'; header.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; header.style.fontWeight = 'bold'; header.innerText = `Detected Videos (${videos.length})`; content.appendChild(header); videos.forEach(v => { const row = document.createElement('div'); row.className = 'uvs-item'; row.innerHTML = `