// ==UserScript== // @name Universal Video Downloader // @namespace http://tampermonkey.net/ // @version 12.0 // @description Download videos. Supports M3U8. Mobile/Desktop action loops. No emojis. // @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'; // ========================================== // ICONS (UNICODE ONLY - NO EMOJIS) // ========================================== const ICONS = { down: '\u2193', // ↓ menu: '\u2630', // ☰ check: '\u2713', // ✓ cross: '\u2715', // ✕ info: '\u2139', // ℹ eye: '\u2299', // ⊙ reload: '\u21BB', // ↻ share: '\u2197', // ↗ copy: '\u2398' // ⎘ (approx) or just use text }; // ========================================== // CONFIGURATION // ========================================== let floatingButton = null; let hiddenToggle = null; // State let isHidden = GM_getValue('uvs_hidden', false); let pressStartTime = 0; let actionCycleIndex = 0; // Tracks the Share/Copy/Download loop // Data Stores const detectedUrls = new Set(); const allDetectedVideos = new Map(); const downloadedBlobs = new Map(); // Settings const CONCURRENCY = 3; const MAX_RETRIES = 3; // Mobile Detection (User Provided Regex) const checkMobile = (a) => { return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)); }; const isMobile = checkMobile(navigator.userAgent || navigator.vendor || window.opera); const isTwitter = location.hostname.includes('twitter.com') || location.hostname.includes('x.com'); // Theme const THEME = { bg: 'rgba(20, 20, 20, 0.75)', 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, timestamp: Date.now(), manifest: data.manifest || null }; allDetectedVideos.set(fullUrl, videoObj); // Notification removed as requested 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); registerVideo({ type: 'm3u8', url: fullUrl, manifest: manifest, duration: duration, size: 0, source: 'M3U8' }); } catch(e) {} } // ========================================== // SORTING & UI // ========================================== function getSortedVideos() { return Array.from(allDetectedVideos.values()).sort((a, b) => { if (a.size > 0 || b.size > 0) return b.size - a.size; const resA = (a.width || 0) * (a.height || 0); const resB = (b.width || 0) * (b.height || 0); if (resB !== resA) return resB - resA; const durA = a.duration || 0; const durB = b.duration || 0; if (durB !== durA) return durB - durA; 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 = ICONS.down; 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 = ICONS.eye; }, 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(ICONS.cross + ' 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 ? ICONS.menu : ICONS.down; } 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 = ICONS.info; if (type === 'success') { div.style.borderColor = THEME.success; div.style.color = THEME.success; icon = ICONS.check; } if (type === 'error') { div.style.borderColor = THEME.error; div.style.color = THEME.error; icon = ICONS.cross; } // Ensure no double icons if msg already has one 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 = `
${v.filename}
${badges}
${ICONS.down}
`; item.onclick = () => { overlay.remove(); processVideo(v); }; list.appendChild(item); }); modal.appendChild(list); overlay.appendChild(modal); document.body.appendChild(overlay); // Close on outside click overlay.onclick = (e) => { if(e.target === overlay) overlay.remove(); }; } // ========================================== // DOWNLOAD LOGIC // ========================================== let ffmpegInstance = null; let ffmpegLoaded = false; let wasmBinaryCache = null; async function initFFmpeg() { if (ffmpegLoaded && ffmpegInstance) return ffmpegInstance; notify(ICONS.reload + ' 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(ICONS.reload + ' 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; } } async function processVideo(video, customName = null) { const filename = customName || sanitizeFilename(document.title); if (video.type === 'direct') { handleFinalOutput(null, video.url, filename); } else { downloadM3U8(video, filename); } } async function downloadM3U8(video, filename) { if (downloadedBlobs.has(video.url)) return handleFinalOutput(downloadedBlobs.get(video.url), null, filename); notify(ICONS.reload + ' Downloading...'); updateProgress(0); try { const segments = video.manifest.segments; const baseUrl = video.url; const results = new Array(segments.length); let completed = 0; let currentIndex = 0; let hasError = false; const worker = async () => { while (currentIndex < segments.length && !hasError) { const i = currentIndex++; const segUrl = resolveUrl(baseUrl, segments[i].uri); let attempts = 0; let success = false; while(attempts < MAX_RETRIES && !success) { try { const res = await fetch(segUrl); if (!res.ok) throw new Error(`Status ${res.status}`); results[i] = await res.arrayBuffer(); success = true; } catch(e) { attempts++; if (attempts === MAX_RETRIES) { hasError = true; throw e; } await new Promise(r => setTimeout(r, 1000)); } } completed++; updateProgress(Math.round((completed / segments.length) * 100)); } }; const workers = []; for (let k = 0; k < CONCURRENCY; k++) workers.push(worker()); await Promise.all(workers); if (hasError) throw new Error("Network errors"); notify(ICONS.reload + ' Stitching...'); const mergedBlob = new Blob(results, { type: 'video/mp2t' }); const mp4Blob = await convertToMP4(mergedBlob, filename); downloadedBlobs.set(video.url, mp4Blob); handleFinalOutput(mp4Blob, null, filename); updateProgress(0); notify('Complete', 'success'); } catch(e) { notify('Error: ' + e.message, 'error'); updateProgress(0); } } function handleFinalOutput(blob, url, filename) { const finalName = filename.endsWith('.mp4') ? filename : filename + '.mp4'; // Define cycles // Mobile: Share -> Copy -> Download // Desktop: Copy -> Share -> Download const modes = isMobile ? ['share', 'copy', 'download'] : ['copy', 'share', 'download']; const currentMode = modes[actionCycleIndex % 3]; actionCycleIndex++; // Increment for next click // --- ACTION: SHARE --- if (currentMode === 'share') { if (navigator.share) { const shareData = { title: finalName }; if (blob) { const file = new File([blob], finalName, { type: 'video/mp4' }); if (navigator.canShare && navigator.canShare({ files: [file] })) { shareData.files = [file]; } else { shareData.text = url || "Video File"; } } else { shareData.url = url; } navigator.share(shareData) .then(() => notify('Shared', 'success')) .catch((err) => { // Mobile fallback requested: Copy error to clipboard if (isMobile) { navigator.clipboard.writeText(err.message || "Share Failed") .then(() => notify('Share Failed: Error Copied', 'error')); } else { notify('Share Cancelled', 'info'); } }); } else { notify('Share not supported', 'error'); } return; } // --- ACTION: COPY --- if (currentMode === 'copy') { const textToCopy = url || "Video Blob (Cannot copy blob URL)"; navigator.clipboard.writeText(textToCopy) .then(() => notify('Link Copied', 'success')) .catch(() => notify('Copy Failed', 'error')); return; } // --- ACTION: DOWNLOAD --- if (currentMode === 'download') { try { const downloadUrl = blob ? URL.createObjectURL(blob) : url; const a = document.createElement('a'); a.href = downloadUrl; a.download = finalName; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); if(blob) URL.revokeObjectURL(downloadUrl); }, 1000); notify('Saved to Disk', 'success'); } catch(e) { notify('Download Error', 'error'); } return; } } // ========================================== // INIT // ========================================== setInterval(scanDOM, 2000); const obs = new MutationObserver(scanDOM); obs.observe(document.body, { childList: true, subtree: true }); if (isTwitter) { const twObs = new MutationObserver(() => { document.querySelectorAll('article[data-testid="tweet"]').forEach(tweet => { const grp = tweet.querySelector('div[role="group"]'); if (!grp || grp.querySelector('.uvs-tw-btn')) return; const btn = document.createElement('div'); btn.className = 'uvs-tw-btn'; btn.innerHTML = ``; btn.onclick = (e) => { e.stopPropagation(); const vids = getSortedVideos().filter(v => v.type === 'm3u8'); if(vids.length) processVideo(vids[0], sanitizeFilename(`twitter_${Date.now()}`)); else notify('Play video first', 'info'); }; grp.appendChild(btn); }); }); twObs.observe(document.body, { childList: true, subtree: true }); } })();