HLS media clipper specialized for MissAV. Features a responsive Material UI, overlay protection, and scrollable jump intervals for seamless video slicing.
// ==UserScript== // @name MissAV Precision Clipper (Material Edition) // @namespace https://github.com/gentlemanan/missav-downloader // @version 9.6 // @description HLS media clipper specialized for MissAV. Features a responsive Material UI, overlay protection, and scrollable jump intervals for seamless video slicing. // @author gentlemanan // @match *://missav.com/* // @match *://*.missav.com/* // @match *://missav.ws/* // @match *://*.missav.ws/* // @match *://missav.ai/* // @match *://*.missav.ai/* // @match *://missav.live/* // @match *://*.missav.live/* // @match *://missav123.com/* // @match *://*.missav123.com/* // @grant unsafeWindow // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/mux.min.js // @run-at document-start // @icon https://www.google.com/s2/favicons?sz=64&domain=missav.com // @license MIT // ==/UserScript== (function () { 'use strict'; // ========================================== // UTILITIES // ========================================== class Utils { static formatTime(sec) { if (sec === null || isNaN(sec)) return '--:--'; const m = Math.floor(sec / 60).toString().padStart(2, '0'); const s = Math.floor(sec % 60).toString().padStart(2, '0'); return `${m}:${s}`; } static saveFile(blob, startTime, endTime, ext) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const s = Utils.formatTime(startTime).replace(':', 'm') + 's'; const e = Utils.formatTime(endTime).replace(':', 'm') + 's'; a.download = `MissAV_Clip_${s}_to_${e}.${ext}`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); } } // ========================================== // NETWORK INTERCEPTOR // ========================================== class NetworkInterceptor { constructor(windowContext) { this.context = windowContext || window; this.m3u8Url = null; } init() { this._interceptFetch(); this._interceptXHR(); } getInterceptedUrl() { return this.m3u8Url; } _interceptFetch() { const originFetch = this.context.fetch; const self = this; this.context.fetch = async function (input, init) { const url = input instanceof Request ? input.url : input.toString(); if (url.includes('.m3u8')) self.m3u8Url = url; return originFetch.apply(this, arguments); }; } _interceptXHR() { const originXHR = this.context.XMLHttpRequest; const self = this; class XHRInterceptor extends originXHR { open(method, url) { if (url && url.toString().includes('.m3u8')) { self.m3u8Url = url.toString(); } super.open(...arguments); } } this.context.XMLHttpRequest = XHRInterceptor; } } // ========================================== // HLS PROCESSOR // ========================================== class HLSProcessor { constructor() { if (typeof muxjs === 'undefined') { console.error('[Clipper] mux.js library not loaded.'); } } async processClip(m3u8Url, startT, endT, uiManager) { if (!m3u8Url) throw new Error('No HLS stream (.m3u8) intercepted.'); if (startT === null || endT === null || startT >= endT) throw new Error('Invalid Start/End times.'); uiManager.showToast('Fetching & Muxing HLS...'); let playlistText = await (await fetch(m3u8Url)).text(); let baseUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf('/') + 1); if (playlistText.includes('#EXT-X-STREAM-INF')) { const mediaUrl = playlistText.split('\n').find((l, i, arr) => arr[i - 1]?.includes('#EXT-X-STREAM-INF')).trim(); const finalUrl = mediaUrl.startsWith('http') ? mediaUrl : baseUrl + mediaUrl; baseUrl = finalUrl.substring(0, finalUrl.lastIndexOf('/') + 1); playlistText = await (await fetch(finalUrl)).text(); } if (playlistText.includes('#EXT-X-KEY:METHOD=AES-128')) { throw new Error('Encrypted stream detected. Cannot download raw chunks.'); } let time = 0; const segments = playlistText.split('\n').reduce((acc, line) => { if (line.startsWith('#EXTINF:')) { acc.duration = parseFloat(line.split(':')[1]); } else if (line && !line.startsWith('#')) { time += acc.duration; if (time > startT && (time - acc.duration) < endT) { acc.urls.push(line.startsWith('http') ? line : baseUrl + line); } } return acc; }, { duration: 0, urls: [] }).urls; if (!segments.length) throw new Error('No segments found in this timeframe.'); const buffers = []; for (const url of segments) { buffers.push(new Uint8Array(await (await fetch(url)).arrayBuffer())); } const transmuxer = new muxjs.mp4.Transmuxer(); let initSegment = null; const dataSegments = []; transmuxer.on('data', (s) => { if (!initSegment) initSegment = s.initSegment; dataSegments.push(s.data); }); const combined = new Uint8Array(buffers.reduce((a, b) => a + b.length, 0)); let offset = 0; for (const b of buffers) { combined.set(b, offset); offset += b.length; } transmuxer.push(combined); transmuxer.flush(); if (!initSegment) throw new Error('Transmuxing failed.'); Utils.saveFile(new Blob([initSegment, ...dataSegments], { type: 'video/mp4' }), startT, endT, 'mp4'); uiManager.showToast('✅ Download Complete!'); } } // ========================================== // UI MANAGER (Material Design) // ========================================== class UIManager { constructor(appInstance) { this.app = appInstance; this.fab = null; this.overlay = null; this._onFullscreenChange = this._handleFullscreenChange.bind(this); this._injectMaterialCSS(); } _injectMaterialCSS() { const css = ` :root { --google-blue: #1a73e8; --google-surface: #ffffff; --google-text: #202124; --md-shadow-2: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); --md-shadow-4: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); } .mat-fab { position: fixed; right: 24px; bottom: 24px; width: 56px; height: 56px; border-radius: 50%; background: var(--google-surface); color: var(--google-blue); z-index: 2147483640; display: flex; align-items: center; justify-content: center; box-shadow: var(--md-shadow-2); cursor: pointer; user-select: none; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); border: none; } .mat-fab:hover { box-shadow: var(--md-shadow-4); transform: translateY(-2px); } .mat-fab svg { width: 24px; height: 24px; fill: currentColor; } .mat-theater-overlay { position: fixed; inset: 0; width: 100vw; height: 100vh; background: #000; z-index: 2147483645; overflow: hidden; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; flex-direction: column; } .mat-video-wrapper { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; min-height: 0; cursor: pointer; overflow: hidden; } .mat-video-wrapper video { width: 100% !important; height: 100% !important; object-fit: contain !important; background: #000 !important; pointer-events: none; } .mat-top-bar { position: absolute; top: 16px; right: 24px; z-index: 10; pointer-events: auto; } .mat-icon-btn { width: 48px; height: 48px; background: rgba(20,20,20,0.6); color: #fff; border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; backdrop-filter: blur(8px); } .mat-icon-btn:hover { background: rgba(220, 40, 40, 0.8); } .mat-icon-btn svg { width: 28px; height: 28px; fill: currentColor; } .mat-controls-bar { flex: 0 0 auto; background: #101010; border-top: 1px solid #222; padding: 20px 24px; display: flex; flex-direction: column; align-items: center; z-index: 2; pointer-events: auto; } .mat-controls-inner { width: 100%; max-width: 900px; display: flex; flex-direction: column; gap: 16px; } .mat-progress-container { display: flex; align-items: center; gap: 16px; color: #fff; font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums; } .mat-slider { flex: 1; -webkit-appearance: none; height: 4px; border-radius: 2px; background: rgba(255,255,255,0.3); outline: none; cursor: pointer; } .mat-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: var(--google-blue); cursor: pointer; transition: transform 0.1s; } .mat-slider::-webkit-slider-thumb:hover { transform: scale(1.3); } .mat-toolbar { display: flex; align-items: center; gap: 12px; justify-content: center; } /* Tooltip & Jump Controls */ .mat-jump-container { display: flex; align-items: center; gap: 4px; } .mat-jump-nav-btn { background: transparent; border: 1px solid rgba(255,255,255,0.2); color: #fff; width: 34px; height: 34px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; transition: all 0.2s; } .mat-jump-nav-btn:hover { background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.4); } .mat-tooltip-wrap { position: relative; display: flex; align-items: center; } .mat-tooltip-wrap::before { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translate(-50%, -8px); background: rgba(30, 30, 30, 0.95); color: #fff; padding: 6px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; font-weight: 500; opacity: 0; pointer-events: none; transition: opacity 0.2s ease, transform 0.2s ease; border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 8px 16px rgba(0,0,0,0.4); z-index: 100; } .mat-tooltip-wrap:hover::before { opacity: 1; transform: translate(-50%, -4px); } .mat-jump-input { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.1); color: #fff; width: 56px; height: 34px; border-radius: 16px; text-align: center; font-weight: 600; font-size: 14px; font-family: inherit; font-variant-numeric: tabular-nums; transition: border-color 0.2s, background 0.2s; outline: none; box-sizing: border-box; } .mat-jump-input:focus { border-color: var(--google-blue); background: rgba(255,255,255,0.15); } .mat-jump-input:hover { background: rgba(255,255,255,0.15); } .mat-jump-input::-webkit-inner-spin-button, .mat-jump-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } .mat-btn { background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 6px 16px; cursor: pointer; height: 34px; box-sizing: border-box; font-size: 14px; font-weight: 500; transition: all 0.2s; display: flex; align-items: center; gap: 8px; font-family: inherit; } .mat-btn:hover { background: rgba(255,255,255,0.2); } .mat-btn.primary { background: var(--google-surface); color: var(--google-blue); border: none; font-weight: 600; } .mat-btn.primary:hover { background: #e8f0fe; } .mat-btn.primary:disabled { opacity: 0.5; cursor: not-allowed; } .mat-toast { position: fixed; bottom: 120px; left: 50%; transform: translateX(-50%); background: #323232; color: #fff; padding: 14px 24px; border-radius: 8px; z-index: 2147483647; font-size: 14px; font-weight: 500; box-shadow: var(--md-shadow-4); font-family: inherit; } `; const style = document.createElement('style'); style.textContent = css; document.documentElement.appendChild(style); } showToast(message) { const el = document.createElement('div'); el.className = 'mat-toast'; el.textContent = message; document.documentElement.appendChild(el); setTimeout(() => el.remove(), 4000); } renderFAB() { if (document.getElementById('mat-clipper-fab')) return; this.fab = document.createElement('button'); this.fab.id = 'mat-clipper-fab'; this.fab.className = 'mat-fab'; this.fab.title = 'Open HLS Clipper'; this.fab.innerHTML = `<svg viewBox="0 0 24 24"><path d="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3z"/></svg>`; this.fab.addEventListener('click', () => this.app.openTheaterMode()); document.documentElement.appendChild(this.fab); } toggleFAB(visible) { if (this.fab) this.fab.style.display = visible ? 'flex' : 'none'; } renderTheater(videoElement) { this.overlay = document.createElement('div'); this.overlay.className = 'mat-theater-overlay'; this.overlay.innerHTML = ` <div class="mat-top-bar"> <button class="mat-icon-btn" id="mat-btn-close" title="Close Theater"> <svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> </button> </div> <div class="mat-video-wrapper" id="mat-video-wrap"></div> <div class="mat-controls-bar"> <div class="mat-controls-inner"> <div class="mat-progress-container"> <span id="mat-lbl-cur">00:00</span> <input type="range" class="mat-slider" id="mat-seek" min="0" max="1000" value="0"> <span id="mat-lbl-dur">00:00</span> </div> <div class="mat-toolbar"> <button class="mat-btn" id="mat-btn-play"> <svg style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg> Play/Pause </button> <div class="mat-jump-container"> <button class="mat-jump-nav-btn" id="mat-btn-jump-back" title="Jump Backward">«</button> <div class="mat-tooltip-wrap" data-tooltip="Scroll to adjust interval (sec)"> <input type="number" class="mat-jump-input" id="mat-input-jump" value="10" min="1" max="120" aria-label="Jump interval"> </div> <button class="mat-jump-nav-btn" id="mat-btn-jump-fwd" title="Jump Forward">»</button> </div> <button class="mat-btn" id="mat-btn-start">Set Start</button> <button class="mat-btn" id="mat-btn-end">Set End</button> <button class="mat-btn" id="mat-btn-fullscreen"> <svg id="mat-icon-fullscreen" style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg> Fullscreen </button> <div style="flex:1"></div> <button class="mat-btn primary" id="mat-btn-render">Export MP4 Clip</button> </div> </div> </div> `; document.documentElement.appendChild(this.overlay); document.getElementById('mat-video-wrap').appendChild(videoElement); this._bindTheaterEvents(videoElement); document.addEventListener('fullscreenchange', this._onFullscreenChange); } destroyTheater() { document.removeEventListener('fullscreenchange', this._onFullscreenChange); if (document.fullscreenElement) document.exitFullscreen().catch(()=>{}); if (this.overlay) { this.overlay.remove(); this.overlay = null; } } _handleFullscreenChange() { const btnFullscreen = document.getElementById('mat-btn-fullscreen'); if (!btnFullscreen) return; if (document.fullscreenElement) { btnFullscreen.innerHTML = '<svg style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg> Exit Fullscreen'; } else { btnFullscreen.innerHTML = '<svg style="width:18px;height:18px;fill:currentColor;" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg> Fullscreen'; } } _bindTheaterEvents(video) { const btnPlay = document.getElementById('mat-btn-play'); const btnStart = document.getElementById('mat-btn-start'); const btnEnd = document.getElementById('mat-btn-end'); const btnRender = document.getElementById('mat-btn-render'); const btnClose = document.getElementById('mat-btn-close'); const btnFullscreen = document.getElementById('mat-btn-fullscreen'); const videoWrap = document.getElementById('mat-video-wrap'); const seekSlider = document.getElementById('mat-seek'); const lblCur = document.getElementById('mat-lbl-cur'); const lblDur = document.getElementById('mat-lbl-dur'); // Jump Elements const jumpInput = document.getElementById('mat-input-jump'); const btnJumpBack = document.getElementById('mat-btn-jump-back'); const btnJumpFwd = document.getElementById('mat-btn-jump-fwd'); const togglePlay = () => video.paused ? video.play() : video.pause(); btnPlay.onclick = togglePlay; videoWrap.onclick = togglePlay; btnClose.onclick = () => this.app.closeTheaterMode(); // Handle << and >> Buttons const getJumpValue = () => parseFloat(jumpInput.value) || 10; btnJumpBack.onclick = () => { video.currentTime = Math.max(0, video.currentTime - getJumpValue()); }; btnJumpFwd.onclick = () => { video.currentTime = Math.min(video.duration, video.currentTime + getJumpValue()); }; // Scroll to adjust the number input jumpInput.addEventListener('wheel', (e) => { e.preventDefault(); const step = e.deltaY > 0 ? -1 : 1; let newVal = (parseInt(jumpInput.value) || 10) + step; newVal = Math.max(1, Math.min(120, newVal)); jumpInput.value = newVal; }); btnFullscreen.onclick = () => { if (!document.fullscreenElement) { this.overlay.requestFullscreen().catch(err => { this.showToast(`Error enabling fullscreen: ${err.message}`); }); } else { document.exitFullscreen(); } }; btnStart.onclick = () => { this.app.setClipStart(video.currentTime); btnStart.innerText = `Start: ${Utils.formatTime(video.currentTime)}`; }; btnEnd.onclick = () => { this.app.setClipEnd(video.currentTime); btnEnd.innerText = `End: ${Utils.formatTime(video.currentTime)}`; }; btnRender.onclick = () => { btnRender.disabled = true; const origText = btnRender.innerText; btnRender.innerText = 'Processing...'; this.app.executeRender().catch(err => { this.showToast(`Error: ${err.message}`); console.error('[Clipper]', err); }).finally(() => { btnRender.disabled = false; btnRender.innerText = origText; }); }; video.addEventListener('timeupdate', () => { if (isNaN(video.duration)) return; lblCur.innerText = Utils.formatTime(video.currentTime); lblDur.innerText = Utils.formatTime(video.duration); if (document.activeElement !== seekSlider) { seekSlider.value = (video.currentTime / video.duration) * 1000; } }); seekSlider.addEventListener('input', () => { if (isNaN(video.duration)) return; video.currentTime = (seekSlider.value / 1000) * video.duration; }); } } // ========================================== // MAIN APP CONTROLLER // ========================================== class VideoClipperApp { constructor() { this.interceptor = new NetworkInterceptor(unsafeWindow); this.ui = new UIManager(this); this.processor = new HLSProcessor(); this.videoData = { element: null, originalParent: null, originalSibling: null, originalStyles: '', originalControls: true }; this.clipState = { start: null, end: null }; this.theaterHiddenElements = []; } init() { this.interceptor.init(); const boot = () => { this.ui.renderFAB(); setInterval(() => this.ui.renderFAB(), 2000); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } } _findTargetVideo() { const missAvPlayer = document.querySelector('video.plyr, video#player, .plyr__video-wrapper video, .video-js video'); if (missAvPlayer) return missAvPlayer; const videos = Array.from(document.querySelectorAll('video')); return videos.sort((a, b) => (b.clientWidth * b.clientHeight) - (a.clientWidth * a.clientHeight))[0]; } _hidePageForTheater() { this.theaterHiddenElements = []; const keepNodes = new Set([this.ui.overlay, this.ui.fab]); Array.from(document.body.children).forEach(el => { if (keepNodes.has(el) || el.classList.contains('mat-theater-overlay')) return; this.theaterHiddenElements.push({ el, visibility: el.style.visibility, pointerEvents: el.style.pointerEvents, opacity: el.style.opacity }); el.style.visibility = 'hidden'; el.style.pointerEvents = 'none'; el.style.opacity = '0'; }); Array.from(document.querySelectorAll('body *')).forEach(el => { if (keepNodes.has(el) || el.closest('.mat-theater-overlay')) return; const style = window.getComputedStyle(el); const zIndex = parseInt(style.zIndex, 10); if ((style.position === 'fixed' || style.position === 'sticky') && (!isNaN(zIndex) && zIndex >= 100)) { this.theaterHiddenElements.push({ el, visibility: el.style.visibility, pointerEvents: el.style.pointerEvents, opacity: el.style.opacity }); el.style.visibility = 'hidden'; el.style.pointerEvents = 'none'; el.style.opacity = '0'; } }); } _restorePageAfterTheater() { this.theaterHiddenElements.forEach(item => { item.el.style.visibility = item.visibility; item.el.style.pointerEvents = item.pointerEvents; item.el.style.opacity = item.opacity; }); this.theaterHiddenElements = []; } openTheaterMode() { const vid = this._findTargetVideo(); if (!vid) return this.ui.showToast('No video found on page!'); this.videoData = { element: vid, originalParent: vid.parentElement, originalSibling: vid.nextSibling, originalStyles: vid.getAttribute('style') || '', originalControls: vid.controls }; this.clipState = { start: null, end: null }; vid.removeAttribute('style'); vid.controls = false; this.ui.toggleFAB(false); document.body.style.overflow = 'hidden'; this._hidePageForTheater(); this.ui.renderTheater(vid); } closeTheaterMode() { const { element, originalParent, originalSibling, originalStyles, originalControls } = this.videoData; if (element) { element.setAttribute('style', originalStyles); element.controls = originalControls; if (originalParent) { originalParent.insertBefore(element, originalSibling); } } this.ui.destroyTheater(); this._restorePageAfterTheater(); this.ui.toggleFAB(true); document.body.style.overflow = ''; this.videoData.element = null; } setClipStart(time) { this.clipState.start = time; } setClipEnd(time) { this.clipState.end = time; } async executeRender() { const m3u8Url = this.interceptor.getInterceptedUrl(); await this.processor.processClip(m3u8Url, this.clipState.start, this.clipState.end, this.ui); } } // ========================================== // BOOTSTRAP // ========================================== new VideoClipperApp().init(); })();