Adult Time Video URL Extractor & Downloader

Extract HLS/m3u8/MP4 video URLs with pagination, quality selection, and batch download support

Size

43.1 KB

Version

1.1.3

Created

Nov 14, 2025

Updated

28 days ago

1// ==UserScript==
2// @name		Adult Time Video URL Extractor & Downloader
3// @description		Extract HLS/m3u8/MP4 video URLs with pagination, quality selection, and batch download support
4// @version		1.1.3
5// @match		https://*.members.adulttime.com/*
6// @icon		https://static03-cms-fame.gammacdn.com/adulttime/m/9tylqmdgtn8cgc0g/favicon-16x16.png
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.xmlHttpRequest
10// @grant		GM.setClipboard
11// @grant		GM.openInTab
12// ==/UserScript==
13(function() {
14    'use strict';
15
16    // ============================================
17    // STATE MANAGEMENT
18    // ============================================
19    let state = {
20        isRunning: false,
21        isPaused: false,
22        currentVideoIndex: 0,
23        totalVideosToProcess: 0,
24        processedVideos: 0,
25        extractedURLs: [],
26        currentPage: 1,
27        failedVideos: [],
28        videoQueue: [],
29        returnURL: null
30    };
31
32    let capturedURLs = [];
33    let isCapturing = false;
34
35    // ============================================
36    // UTILITY FUNCTIONS
37    // ============================================
38    
39    function sleep(ms) {
40        return new Promise(resolve => setTimeout(resolve, ms));
41    }
42
43    async function saveState() {
44        await GM.setValue('videoExtractorState', JSON.stringify(state));
45        console.log('[State] Saved:', state);
46    }
47
48    async function loadState() {
49        try {
50            const savedState = await GM.getValue('videoExtractorState', null);
51            if (savedState) {
52                const parsed = JSON.parse(savedState);
53                state = { ...state, ...parsed, isRunning: false, isPaused: false };
54                console.log('[State] Loaded:', state);
55                return true;
56            }
57        } catch (e) {
58            console.error('[State] Error loading:', e);
59        }
60        return false;
61    }
62
63    async function clearState() {
64        state = {
65            isRunning: false,
66            isPaused: false,
67            currentVideoIndex: 0,
68            totalVideosToProcess: 0,
69            processedVideos: 0,
70            extractedURLs: [],
71            currentPage: 1,
72            failedVideos: [],
73            videoQueue: [],
74            returnURL: null
75        };
76        await saveState();
77        console.log('[State] Cleared');
78    }
79
80    // ============================================
81    // NETWORK INTERCEPTION
82    // ============================================
83    
84    function setupNetworkInterceptor() {
85        console.log('[Interceptor] Setting up network capture...');
86        
87        // Intercept fetch requests
88        const originalFetch = window.fetch;
89        window.fetch = function(...args) {
90            const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
91            
92            if (isCapturing && url) {
93                if (url.includes('.m3u8') || url.includes('.mp4') || url.includes('.mpd') || 
94                    url.includes('streaming') || url.includes('gammacdn')) {
95                    console.log('[Interceptor] Captured fetch URL:', url);
96                    capturedURLs.push({ url, type: 'fetch', timestamp: Date.now() });
97                }
98            }
99            
100            return originalFetch.apply(this, args);
101        };
102
103        // Intercept XMLHttpRequest
104        const originalXHROpen = XMLHttpRequest.prototype.open;
105        XMLHttpRequest.prototype.open = function(method, url) {
106            if (isCapturing && url) {
107                if (url.includes('.m3u8') || url.includes('.mp4') || url.includes('.mpd') || 
108                    url.includes('streaming') || url.includes('gammacdn')) {
109                    console.log('[Interceptor] Captured XHR URL:', url);
110                    capturedURLs.push({ url, type: 'xhr', timestamp: Date.now() });
111                }
112            }
113            
114            return originalXHROpen.apply(this, arguments);
115        };
116
117        console.log('[Interceptor] Network capture ready');
118    }
119
120    // ============================================
121    // UI CREATION
122    // ============================================
123    
124    function createControlPanel() {
125        // Remove existing panel if any
126        const existing = document.getElementById('video-url-extractor-panel');
127        if (existing) existing.remove();
128
129        const panel = document.createElement('div');
130        panel.id = 'video-url-extractor-panel';
131        panel.innerHTML = `
132            <div class="vex-header">
133                <h3>🎬 Video URL Extractor</h3>
134                <button id="vex-minimize" title="Minimize"></button>
135            </div>
136            <div class="vex-content">
137                <div class="vex-section">
138                    <label for="vex-video-count">Number of videos to extract:</label>
139                    <input type="number" id="vex-video-count" min="1" value="10" />
140                </div>
141                
142                <div class="vex-section">
143                    <label for="vex-quality">Preferred Quality:</label>
144                    <select id="vex-quality">
145                        <option value="1080">1080p (Preferred)</option>
146                        <option value="720">720p</option>
147                        <option value="480">480p</option>
148                        <option value="highest">Highest Available</option>
149                    </select>
150                </div>
151
152                <div class="vex-section">
153                    <label>
154                        <input type="checkbox" id="vex-auto-pagination" checked />
155                        Auto-paginate to find videos
156                    </label>
157                </div>
158
159                <div class="vex-section">
160                    <label>
161                        <input type="checkbox" id="vex-skip-processed" checked />
162                        Skip already processed videos
163                    </label>
164                </div>
165
166                <div class="vex-controls">
167                    <button id="vex-start" class="vex-btn vex-btn-primary">▶ Start Extraction</button>
168                    <button id="vex-pause" class="vex-btn vex-btn-warning" disabled>⏸ Pause</button>
169                    <button id="vex-stop" class="vex-btn vex-btn-danger" disabled>⏹ Stop</button>
170                    <button id="vex-resume" class="vex-btn vex-btn-success" style="display:none;">▶ Resume</button>
171                </div>
172
173                <div class="vex-progress-section">
174                    <div class="vex-progress-info">
175                        <span id="vex-status">Ready</span>
176                        <span id="vex-progress-text">0/0</span>
177                    </div>
178                    <div class="vex-progress-bar">
179                        <div id="vex-progress-fill"></div>
180                    </div>
181                    <div id="vex-current-video" class="vex-current-video"></div>
182                </div>
183
184                <div class="vex-results-section">
185                    <div class="vex-results-header">
186                        <h4>Extracted URLs (<span id="vex-url-count">0</span>)</h4>
187                        <div class="vex-results-actions">
188                            <button id="vex-copy-all" class="vex-btn vex-btn-small" disabled>📋 Copy All</button>
189                            <button id="vex-copy-m3u8" class="vex-btn vex-btn-small" disabled>📋 Copy M3U8</button>
190                            <button id="vex-export-txt" class="vex-btn vex-btn-small" disabled>💾 Export TXT</button>
191                            <button id="vex-export-json" class="vex-btn vex-btn-small" disabled>💾 Export JSON</button>
192                            <button id="vex-clear-urls" class="vex-btn vex-btn-small vex-btn-danger" disabled>🗑 Clear</button>
193                        </div>
194                    </div>
195                    <div id="vex-url-list" class="vex-url-list"></div>
196                </div>
197
198                <div class="vex-stats">
199                    <div class="vex-stat">
200                        <span class="vex-stat-label">Success:</span>
201                        <span id="vex-stat-success" class="vex-stat-value">0</span>
202                    </div>
203                    <div class="vex-stat">
204                        <span class="vex-stat-label">Failed:</span>
205                        <span id="vex-stat-failed" class="vex-stat-value">0</span>
206                    </div>
207                    <div class="vex-stat">
208                        <span class="vex-stat-label">Current Page:</span>
209                        <span id="vex-stat-page" class="vex-stat-value">1</span>
210                    </div>
211                </div>
212            </div>
213        `;
214
215        document.body.appendChild(panel);
216        addStyles();
217        attachEventListeners();
218    }
219
220    function addStyles() {
221        if (document.getElementById('vex-styles')) return;
222        
223        const styleSheet = document.createElement('style');
224        styleSheet.id = 'vex-styles';
225        styleSheet.textContent = `
226            #video-url-extractor-panel {
227                position: fixed;
228                top: 20px;
229                right: 20px;
230                width: 450px;
231                max-height: 90vh;
232                background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
233                border: 2px solid #444;
234                border-radius: 12px;
235                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
236                z-index: 999999;
237                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
238                color: #fff;
239                overflow: hidden;
240                backdrop-filter: blur(10px);
241            }
242
243            .vex-header {
244                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
245                padding: 15px 20px;
246                display: flex;
247                justify-content: space-between;
248                align-items: center;
249                cursor: move;
250                user-select: none;
251            }
252
253            .vex-header h3 {
254                margin: 0;
255                font-size: 18px;
256                font-weight: 600;
257            }
258
259            #vex-minimize {
260                background: rgba(255, 255, 255, 0.2);
261                border: none;
262                color: white;
263                width: 30px;
264                height: 30px;
265                border-radius: 50%;
266                cursor: pointer;
267                font-size: 20px;
268                display: flex;
269                align-items: center;
270                justify-content: center;
271                transition: all 0.3s;
272            }
273
274            #vex-minimize:hover {
275                background: rgba(255, 255, 255, 0.3);
276                transform: scale(1.1);
277            }
278
279            .vex-content {
280                padding: 20px;
281                max-height: calc(90vh - 70px);
282                overflow-y: auto;
283            }
284
285            .vex-content::-webkit-scrollbar {
286                width: 8px;
287            }
288
289            .vex-content::-webkit-scrollbar-track {
290                background: #2d2d2d;
291            }
292
293            .vex-content::-webkit-scrollbar-thumb {
294                background: #667eea;
295                border-radius: 4px;
296            }
297
298            .vex-section {
299                margin-bottom: 15px;
300            }
301
302            .vex-section label {
303                display: block;
304                margin-bottom: 5px;
305                font-size: 13px;
306                color: #ccc;
307            }
308
309            .vex-section input[type="number"],
310            .vex-section select {
311                width: 100%;
312                padding: 10px;
313                background: #3a3a3a;
314                border: 1px solid #555;
315                border-radius: 6px;
316                color: #fff;
317                font-size: 14px;
318                transition: all 0.3s;
319            }
320
321            .vex-section input[type="number"]:focus,
322            .vex-section select:focus {
323                outline: none;
324                border-color: #667eea;
325                box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
326            }
327
328            .vex-section input[type="checkbox"] {
329                margin-right: 8px;
330                cursor: pointer;
331            }
332
333            .vex-controls {
334                display: flex;
335                gap: 8px;
336                margin: 20px 0;
337                flex-wrap: wrap;
338            }
339
340            .vex-btn {
341                flex: 1;
342                padding: 10px 15px;
343                border: none;
344                border-radius: 6px;
345                cursor: pointer;
346                font-size: 13px;
347                font-weight: 600;
348                transition: all 0.3s;
349                min-width: 80px;
350            }
351
352            .vex-btn:disabled {
353                opacity: 0.5;
354                cursor: not-allowed;
355            }
356
357            .vex-btn-primary {
358                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
359                color: white;
360            }
361
362            .vex-btn-primary:hover:not(:disabled) {
363                transform: translateY(-2px);
364                box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
365            }
366
367            .vex-btn-warning {
368                background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
369                color: white;
370            }
371
372            .vex-btn-danger {
373                background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
374                color: white;
375            }
376
377            .vex-btn-success {
378                background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
379                color: white;
380            }
381
382            .vex-btn-small {
383                padding: 6px 10px;
384                font-size: 11px;
385                min-width: auto;
386            }
387
388            .vex-progress-section {
389                background: #2a2a2a;
390                padding: 15px;
391                border-radius: 8px;
392                margin: 15px 0;
393            }
394
395            .vex-progress-info {
396                display: flex;
397                justify-content: space-between;
398                margin-bottom: 8px;
399                font-size: 13px;
400            }
401
402            #vex-status {
403                color: #4facfe;
404                font-weight: 600;
405            }
406
407            .vex-progress-bar {
408                height: 8px;
409                background: #3a3a3a;
410                border-radius: 4px;
411                overflow: hidden;
412            }
413
414            #vex-progress-fill {
415                height: 100%;
416                background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
417                width: 0%;
418                transition: width 0.3s;
419            }
420
421            .vex-current-video {
422                margin-top: 10px;
423                font-size: 12px;
424                color: #aaa;
425                font-style: italic;
426                min-height: 18px;
427            }
428
429            .vex-results-section {
430                margin: 20px 0;
431            }
432
433            .vex-results-header {
434                display: flex;
435                justify-content: space-between;
436                align-items: center;
437                margin-bottom: 10px;
438                flex-wrap: wrap;
439                gap: 10px;
440            }
441
442            .vex-results-header h4 {
443                margin: 0;
444                font-size: 15px;
445            }
446
447            .vex-results-actions {
448                display: flex;
449                gap: 5px;
450                flex-wrap: wrap;
451            }
452
453            .vex-url-list {
454                background: #2a2a2a;
455                border-radius: 8px;
456                max-height: 300px;
457                overflow-y: auto;
458                padding: 10px;
459            }
460
461            .vex-url-list::-webkit-scrollbar {
462                width: 6px;
463            }
464
465            .vex-url-list::-webkit-scrollbar-thumb {
466                background: #667eea;
467                border-radius: 3px;
468            }
469
470            .vex-url-item {
471                background: #3a3a3a;
472                padding: 10px;
473                margin-bottom: 8px;
474                border-radius: 6px;
475                font-size: 12px;
476                border-left: 3px solid #667eea;
477            }
478
479            .vex-url-item-title {
480                font-weight: 600;
481                color: #fff;
482                margin-bottom: 5px;
483                display: flex;
484                justify-content: space-between;
485                align-items: center;
486            }
487
488            .vex-url-item-quality {
489                background: #667eea;
490                padding: 2px 8px;
491                border-radius: 4px;
492                font-size: 10px;
493                font-weight: 600;
494            }
495
496            .vex-url-item-url {
497                color: #4facfe;
498                word-break: break-all;
499                font-family: monospace;
500                font-size: 11px;
501            }
502
503            .vex-url-item-actions {
504                margin-top: 8px;
505                display: flex;
506                gap: 5px;
507            }
508
509            .vex-stats {
510                display: flex;
511                justify-content: space-around;
512                background: #2a2a2a;
513                padding: 15px;
514                border-radius: 8px;
515                margin-top: 15px;
516            }
517
518            .vex-stat {
519                text-align: center;
520            }
521
522            .vex-stat-label {
523                display: block;
524                font-size: 11px;
525                color: #aaa;
526                margin-bottom: 5px;
527            }
528
529            .vex-stat-value {
530                display: block;
531                font-size: 20px;
532                font-weight: 700;
533                color: #667eea;
534            }
535
536            .vex-minimized .vex-content {
537                display: none;
538            }
539
540            .vex-minimized {
541                width: 250px;
542            }
543
544            @keyframes pulse {
545                0%, 100% { opacity: 1; }
546                50% { opacity: 0.5; }
547            }
548
549            .vex-pulse {
550                animation: pulse 2s infinite;
551            }
552        `;
553        document.head.appendChild(styleSheet);
554    }
555
556    function attachEventListeners() {
557        // Minimize/Maximize
558        document.getElementById('vex-minimize').addEventListener('click', () => {
559            const panel = document.getElementById('video-url-extractor-panel');
560            panel.classList.toggle('vex-minimized');
561            const btn = document.getElementById('vex-minimize');
562            btn.textContent = panel.classList.contains('vex-minimized') ? '+' : '−';
563        });
564
565        // Make panel draggable
566        makeDraggable(document.getElementById('video-url-extractor-panel'));
567
568        // Control buttons
569        document.getElementById('vex-start').addEventListener('click', startExtraction);
570        document.getElementById('vex-pause').addEventListener('click', pauseExtraction);
571        document.getElementById('vex-stop').addEventListener('click', stopExtraction);
572        document.getElementById('vex-resume').addEventListener('click', resumeExtraction);
573
574        // Export buttons
575        document.getElementById('vex-copy-all').addEventListener('click', () => copyURLs('all'));
576        document.getElementById('vex-copy-m3u8').addEventListener('click', () => copyURLs('m3u8'));
577        document.getElementById('vex-export-txt').addEventListener('click', () => exportURLs('txt'));
578        document.getElementById('vex-export-json').addEventListener('click', () => exportURLs('json'));
579        document.getElementById('vex-clear-urls').addEventListener('click', clearURLs);
580    }
581
582    function makeDraggable(element) {
583        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
584        const header = element.querySelector('.vex-header');
585        
586        header.onmousedown = dragMouseDown;
587
588        function dragMouseDown(e) {
589            e.preventDefault();
590            pos3 = e.clientX;
591            pos4 = e.clientY;
592            document.onmouseup = closeDragElement;
593            document.onmousemove = elementDrag;
594        }
595
596        function elementDrag(e) {
597            e.preventDefault();
598            pos1 = pos3 - e.clientX;
599            pos2 = pos4 - e.clientY;
600            pos3 = e.clientX;
601            pos4 = e.clientY;
602            element.style.top = (element.offsetTop - pos2) + 'px';
603            element.style.left = (element.offsetLeft - pos1) + 'px';
604            element.style.right = 'auto';
605        }
606
607        function closeDragElement() {
608            document.onmouseup = null;
609            document.onmousemove = null;
610        }
611    }
612
613    // ============================================
614    // EXTRACTION LOGIC - IN-PAGE METHOD
615    // ============================================
616
617    async function startExtraction() {
618        const videoCount = parseInt(document.getElementById('vex-video-count').value);
619        if (videoCount < 1) {
620            alert('Please enter a valid number of videos (minimum 1)');
621            return;
622        }
623
624        // Check if we're on a video listing page
625        const isListingPage = window.location.href.includes('/videos') || 
626                              document.querySelectorAll('a.SceneThumb-SceneImageLink-Link[href*="/video/"]').length > 0;
627
628        if (!isListingPage) {
629            alert('Please start from a video listing page (e.g., /videos)');
630            return;
631        }
632
633        // Build video queue from current page
634        const videoLinks = getVideoLinksOnPage();
635        state.videoQueue = videoLinks.slice(0, videoCount).map(link => ({
636            url: link.href,
637            title: link.getAttribute('title') || 'Unknown Video'
638        }));
639
640        state.isRunning = true;
641        state.isPaused = false;
642        state.totalVideosToProcess = Math.min(videoCount, state.videoQueue.length);
643        state.processedVideos = 0;
644        state.currentVideoIndex = 0;
645        state.currentPage = 1;
646        state.returnURL = window.location.href;
647
648        await saveState();
649        updateUI();
650        updateStatus('Starting extraction...', 'info');
651
652        document.getElementById('vex-start').disabled = true;
653        document.getElementById('vex-pause').disabled = false;
654        document.getElementById('vex-stop').disabled = false;
655
656        console.log(`[Extractor] Starting extraction of ${state.totalVideosToProcess} videos`);
657        console.log('[Extractor] Video queue:', state.videoQueue);
658
659        // Navigate to first video
660        if (state.videoQueue.length > 0) {
661            const firstVideo = state.videoQueue[0];
662            console.log(`[Extractor] Navigating to first video: ${firstVideo.title}`);
663            window.location.href = firstVideo.url;
664        }
665    }
666
667    async function pauseExtraction() {
668        state.isPaused = true;
669        await saveState();
670        updateStatus('Paused', 'warning');
671        
672        document.getElementById('vex-pause').style.display = 'none';
673        document.getElementById('vex-resume').style.display = 'block';
674    }
675
676    async function resumeExtraction() {
677        state.isPaused = false;
678        await saveState();
679        updateStatus('Resuming...', 'info');
680        
681        document.getElementById('vex-pause').style.display = 'block';
682        document.getElementById('vex-resume').style.display = 'none';
683
684        // Continue processing
685        await processCurrentVideoPage();
686    }
687
688    async function stopExtraction() {
689        state.isRunning = false;
690        state.isPaused = false;
691        await saveState();
692        updateStatus('Stopped', 'danger');
693        
694        document.getElementById('vex-start').disabled = false;
695        document.getElementById('vex-pause').disabled = true;
696        document.getElementById('vex-stop').disabled = true;
697        document.getElementById('vex-resume').style.display = 'none';
698
699        // Return to listing page
700        if (state.returnURL && !window.location.href.includes('/videos')) {
701            console.log('[Extractor] Returning to listing page...');
702            window.location.href = state.returnURL;
703        }
704    }
705
706    function getVideoLinksOnPage() {
707        const links = document.querySelectorAll('a.SceneThumb-SceneImageLink-Link[href*="/video/"]');
708        return Array.from(links);
709    }
710
711    // ============================================
712    // VIDEO PAGE PROCESSING
713    // ============================================
714
715    async function processCurrentVideoPage() {
716        if (!state.isRunning || state.isPaused) {
717            console.log('[Processor] Not running or paused, skipping');
718            return;
719        }
720
721        const currentVideo = state.videoQueue[state.currentVideoIndex];
722        if (!currentVideo) {
723            console.log('[Processor] No current video, finishing');
724            await finishExtraction();
725            return;
726        }
727
728        console.log(`[Processor] Processing video ${state.currentVideoIndex + 1}/${state.totalVideosToProcess}: ${currentVideo.title}`);
729        updateCurrentVideo(currentVideo.title);
730        updateStatus(`Processing ${state.currentVideoIndex + 1}/${state.totalVideosToProcess}`, 'info');
731
732        // Start capturing network requests
733        capturedURLs = [];
734        isCapturing = true;
735        console.log('[Processor] Started capturing network requests');
736
737        // Wait for page to be fully loaded
738        await sleep(2000);
739
740        // Try to find and click the Watch Episode button
741        const watchButton = document.querySelector('button.ScenePlayerHeaderPlus-WatchEpisodeButton-Button');
742        
743        if (watchButton) {
744            console.log('[Processor] Found Watch Episode button, clicking...');
745            watchButton.click();
746            
747            // Wait for video to start loading
748            await sleep(5000);
749            
750            // Stop capturing
751            isCapturing = false;
752            console.log(`[Processor] Stopped capturing. Found ${capturedURLs.length} URLs`);
753            
754            // Also check Performance API for additional URLs
755            const performanceURLs = getPerformanceVideoURLs();
756            console.log(`[Processor] Found ${performanceURLs.length} URLs from Performance API`);
757            
758            // Combine all captured URLs
759            const allCapturedURLs = [...capturedURLs, ...performanceURLs];
760            console.log(`[Processor] Total captured URLs: ${allCapturedURLs.length}`);
761            
762            if (allCapturedURLs.length > 0) {
763                // Process and select best URL
764                const videoData = selectBestCapturedURL(allCapturedURLs, currentVideo);
765                
766                if (videoData) {
767                    state.extractedURLs.push(videoData);
768                    state.processedVideos++;
769                    console.log(`[Processor] ✓ Successfully extracted URL for: ${currentVideo.title}`);
770                } else {
771                    state.failedVideos.push(currentVideo);
772                    console.warn(`[Processor] ✗ Failed to extract URL for: ${currentVideo.title}`);
773                }
774            } else {
775                state.failedVideos.push(currentVideo);
776                console.warn(`[Processor] ✗ No URLs captured for: ${currentVideo.title}`);
777            }
778        } else {
779            console.warn('[Processor] Watch Episode button not found');
780            state.failedVideos.push(currentVideo);
781        }
782
783        // Update UI
784        updateProgress();
785        updateStats();
786        updateURLList();
787        await saveState();
788
789        // Move to next video
790        state.currentVideoIndex++;
791        
792        if (state.currentVideoIndex < state.videoQueue.length && state.processedVideos < state.totalVideosToProcess) {
793            const nextVideo = state.videoQueue[state.currentVideoIndex];
794            console.log(`[Processor] Moving to next video: ${nextVideo.title}`);
795            await sleep(1000);
796            window.location.href = nextVideo.url;
797        } else {
798            await finishExtraction();
799        }
800    }
801
802    function getPerformanceVideoURLs() {
803        const entries = performance.getEntriesByType('resource');
804        const videoURLs = [];
805        
806        entries.forEach(entry => {
807            const url = entry.name;
808            if (url.includes('.m3u8') || url.includes('.mp4') || url.includes('.mpd') ||
809                url.includes('streaming-hls.gammacdn.com') || url.includes('m3u8.gammacdn.com')) {
810                videoURLs.push({ url, type: 'performance', timestamp: Date.now() });
811                console.log('[Performance] Found URL:', url.substring(0, 100) + '...');
812            }
813        });
814        
815        return videoURLs;
816    }
817
818    function selectBestCapturedURL(capturedURLs, videoInfo) {
819        const preferredQuality = document.getElementById('vex-quality').value;
820        
821        console.log('[Selector] Processing captured URLs...');
822        
823        // Parse and categorize URLs
824        const sources = [];
825        
826        capturedURLs.forEach(item => {
827            let url = item.url;
828            
829            // Decode if it's an encoded gammacdn URL
830            if (url.includes('m3u8.gammacdn.com/?u=')) {
831                try {
832                    const match = url.match(/\?u=([^&]+)/);
833                    if (match) {
834                        const decoded = decodeURIComponent(match[1]);
835                        console.log('[Selector] Decoded URL:', decoded.substring(0, 100) + '...');
836                        url = decoded;
837                    }
838                } catch (e) {
839                    console.warn('[Selector] Failed to decode URL:', e.message);
840                }
841            }
842            
843            // Skip .ts segment files
844            if (url.includes('.ts?') || url.endsWith('.ts')) {
845                return;
846            }
847            
848            // Only keep master m3u8 files (not quality-specific segments)
849            if (url.includes('.m3u8')) {
850                const quality = extractQualityFromURL(url);
851                sources.push({
852                    url: url,
853                    type: 'm3u8',
854                    quality: quality,
855                    method: item.type
856                });
857                console.log(`[Selector] Added m3u8 source: ${quality} - ${url.substring(0, 80)}...`);
858            } else if (url.includes('.mp4')) {
859                const quality = extractQualityFromURL(url);
860                sources.push({
861                    url: url,
862                    type: 'mp4',
863                    quality: quality,
864                    method: item.type
865                });
866                console.log(`[Selector] Added mp4 source: ${quality} - ${url.substring(0, 80)}...`);
867            } else if (url.includes('.mpd')) {
868                const quality = extractQualityFromURL(url);
869                sources.push({
870                    url: url,
871                    type: 'dash',
872                    quality: quality,
873                    method: item.type
874                });
875                console.log(`[Selector] Added dash source: ${quality} - ${url.substring(0, 80)}...`);
876            }
877        });
878
879        // Remove duplicates
880        const uniqueSources = sources.filter((source, index, self) =>
881            index === self.findIndex(s => s.url === source.url)
882        );
883
884        console.log(`[Selector] Found ${uniqueSources.length} unique sources`);
885
886        if (uniqueSources.length === 0) {
887            return null;
888        }
889
890        // Select best source
891        const selectedSource = selectBestSource(uniqueSources, preferredQuality);
892        console.log(`[Selector] Selected: ${selectedSource.quality} ${selectedSource.type}`);
893
894        return {
895            title: videoInfo.title,
896            pageURL: videoInfo.url,
897            videoURL: selectedSource.url,
898            type: selectedSource.type,
899            quality: selectedSource.quality,
900            method: selectedSource.method,
901            allSources: uniqueSources,
902            timestamp: new Date().toISOString()
903        };
904    }
905
906    function extractQualityFromURL(url) {
907        // Try to extract quality from URL
908        const qualityMatch = url.match(/[_-](\d{3,4})[pP]/);
909        if (qualityMatch) {
910            return qualityMatch[1] + 'p';
911        }
912        
913        // Check for quality indicators in URL
914        if (url.includes('2160') || url.includes('4k') || url.includes('uhd')) return '2160p';
915        if (url.includes('1080') || url.includes('fhd')) return '1080p';
916        if (url.includes('720') || url.includes('hd')) return '720p';
917        if (url.includes('576')) return '576p';
918        if (url.includes('480') || url.includes('sd')) return '480p';
919        if (url.includes('360')) return '360p';
920        if (url.includes('240')) return '240p';
921        
922        return 'unknown';
923    }
924
925    function getVideoType(url) {
926        if (url.includes('.m3u8')) return 'm3u8';
927        if (url.includes('.mp4')) return 'mp4';
928        if (url.includes('.mpd')) return 'dash';
929        if (url.includes('.webm')) return 'webm';
930        if (url.includes('.mkv')) return 'mkv';
931        return 'unknown';
932    }
933
934    function selectBestSource(sources, preferredQuality) {
935        console.log(`[Selector] Selecting best source from ${sources.length} options, preferred quality: ${preferredQuality}`);
936        
937        // Prioritize by type: m3u8 > mp4 > dash > others
938        const m3u8Sources = sources.filter(s => s.type === 'm3u8');
939        const mp4Sources = sources.filter(s => s.type === 'mp4');
940        const dashSources = sources.filter(s => s.type === 'dash');
941        const otherSources = sources.filter(s => !['m3u8', 'mp4', 'dash'].includes(s.type));
942
943        let prioritizedSources = [...m3u8Sources, ...mp4Sources, ...dashSources, ...otherSources];
944
945        if (preferredQuality === 'highest') {
946            // Find highest quality
947            const qualityOrder = ['2160p', '1080p', '720p', '576p', '480p', '360p', '240p', 'unknown'];
948            for (const quality of qualityOrder) {
949                const found = prioritizedSources.find(s => s.quality === quality);
950                if (found) {
951                    console.log(`[Selector] Selected highest quality: ${quality}`);
952                    return found;
953                }
954            }
955        } else {
956            // Find preferred quality or closest
957            const targetQuality = preferredQuality + 'p';
958            const found = prioritizedSources.find(s => s.quality === targetQuality);
959            if (found) {
960                console.log(`[Selector] Found exact match for ${targetQuality}`);
961                return found;
962            }
963
964            // Find closest quality (prefer higher)
965            const qualityValue = parseInt(preferredQuality);
966            const sorted = prioritizedSources
967                .filter(s => s.quality !== 'unknown')
968                .sort((a, b) => {
969                    const aVal = parseInt(a.quality);
970                    const bVal = parseInt(b.quality);
971                    const aDiff = Math.abs(aVal - qualityValue);
972                    const bDiff = Math.abs(bVal - qualityValue);
973                    if (aDiff === bDiff) return bVal - aVal; // Prefer higher
974                    return aDiff - bDiff;
975                });
976            
977            if (sorted.length > 0) {
978                console.log(`[Selector] Selected closest quality: ${sorted[0].quality}`);
979                return sorted[0];
980            }
981        }
982
983        // Fallback to first available
984        console.log('[Selector] Using fallback: first available source');
985        return prioritizedSources[0] || sources[0];
986    }
987
988    async function finishExtraction() {
989        console.log('[Extractor] Finishing extraction...');
990        
991        state.isRunning = false;
992        updateStatus('Extraction complete!', 'success');
993        
994        document.getElementById('vex-start').disabled = false;
995        document.getElementById('vex-pause').disabled = true;
996        document.getElementById('vex-stop').disabled = true;
997        document.getElementById('vex-resume').style.display = 'none';
998
999        await saveState();
1000
1001        // Return to listing page
1002        if (state.returnURL && !window.location.href.includes('/videos')) {
1003            console.log('[Extractor] Returning to listing page...');
1004            await sleep(2000);
1005            window.location.href = state.returnURL;
1006        }
1007    }
1008
1009    // ============================================
1010    // UI UPDATE FUNCTIONS
1011    // ============================================
1012
1013    function updateUI() {
1014        updateProgress();
1015        updateStats();
1016        updateURLList();
1017    }
1018
1019    function updateStatus(message) {
1020        const statusEl = document.getElementById('vex-status');
1021        if (statusEl) {
1022            statusEl.textContent = message;
1023            statusEl.className = 'vex-pulse';
1024            
1025            setTimeout(() => {
1026                statusEl.classList.remove('vex-pulse');
1027            }, 2000);
1028        }
1029    }
1030
1031    function updateProgress() {
1032        const progressText = document.getElementById('vex-progress-text');
1033        const progressFill = document.getElementById('vex-progress-fill');
1034        
1035        if (progressText) {
1036            progressText.textContent = `${state.processedVideos}/${state.totalVideosToProcess}`;
1037        }
1038        
1039        if (progressFill) {
1040            const percentage = state.totalVideosToProcess > 0 
1041                ? (state.processedVideos / state.totalVideosToProcess) * 100 
1042                : 0;
1043            progressFill.style.width = percentage + '%';
1044        }
1045    }
1046
1047    function updateCurrentVideo(title) {
1048        const currentVideoEl = document.getElementById('vex-current-video');
1049        if (currentVideoEl) {
1050            currentVideoEl.textContent = `Current: ${title}`;
1051        }
1052    }
1053
1054    function updateStats() {
1055        const successEl = document.getElementById('vex-stat-success');
1056        const failedEl = document.getElementById('vex-stat-failed');
1057        const pageEl = document.getElementById('vex-stat-page');
1058        
1059        if (successEl) successEl.textContent = state.extractedURLs.length;
1060        if (failedEl) failedEl.textContent = state.failedVideos.length;
1061        if (pageEl) pageEl.textContent = state.currentPage;
1062    }
1063
1064    function updateURLList() {
1065        const urlList = document.getElementById('vex-url-list');
1066        const urlCount = document.getElementById('vex-url-count');
1067        
1068        if (!urlList || !urlCount) return;
1069        
1070        urlCount.textContent = state.extractedURLs.length;
1071
1072        if (state.extractedURLs.length === 0) {
1073            urlList.innerHTML = '<div style="text-align: center; color: #888; padding: 20px;">No URLs extracted yet</div>';
1074            disableExportButtons();
1075            return;
1076        }
1077
1078        enableExportButtons();
1079
1080        urlList.innerHTML = state.extractedURLs.map((video, index) => `
1081            <div class="vex-url-item">
1082                <div class="vex-url-item-title">
1083                    <span>${index + 1}. ${video.title}</span>
1084                    <span class="vex-url-item-quality">${video.quality}${video.type.toUpperCase()}</span>
1085                </div>
1086                <div class="vex-url-item-url">${video.videoURL}</div>
1087                <div class="vex-url-item-actions">
1088                    <button class="vex-btn vex-btn-small vex-btn-primary" onclick="navigator.clipboard.writeText('${video.videoURL.replace(/'/g, '\\\'')}')">📋 Copy</button>
1089                    <button class="vex-btn vex-btn-small" onclick="window.open('${video.pageURL}', '_blank')">🔗 Open Page</button>
1090                </div>
1091            </div>
1092        `).join('');
1093    }
1094
1095    function enableExportButtons() {
1096        const buttons = ['vex-copy-all', 'vex-copy-m3u8', 'vex-export-txt', 'vex-export-json', 'vex-clear-urls'];
1097        buttons.forEach(id => {
1098            const btn = document.getElementById(id);
1099            if (btn) btn.disabled = false;
1100        });
1101    }
1102
1103    function disableExportButtons() {
1104        const buttons = ['vex-copy-all', 'vex-copy-m3u8', 'vex-export-txt', 'vex-export-json', 'vex-clear-urls'];
1105        buttons.forEach(id => {
1106            const btn = document.getElementById(id);
1107            if (btn) btn.disabled = true;
1108        });
1109    }
1110
1111    // ============================================
1112    // EXPORT FUNCTIONS
1113    // ============================================
1114
1115    async function copyURLs(type) {
1116        let urls = [];
1117        
1118        if (type === 'all') {
1119            urls = state.extractedURLs.map(v => v.videoURL);
1120        } else if (type === 'm3u8') {
1121            urls = state.extractedURLs.filter(v => v.type === 'm3u8').map(v => v.videoURL);
1122        }
1123
1124        if (urls.length === 0) {
1125            alert('No URLs to copy!');
1126            return;
1127        }
1128
1129        const text = urls.join('\n');
1130        await GM.setClipboard(text);
1131        updateStatus(`Copied ${urls.length} URLs to clipboard!`, 'success');
1132    }
1133
1134    function exportURLs(format) {
1135        if (state.extractedURLs.length === 0) {
1136            alert('No URLs to export!');
1137            return;
1138        }
1139
1140        let content, filename, mimeType;
1141
1142        if (format === 'txt') {
1143            content = state.extractedURLs.map((v, i) => 
1144                `${i + 1}. ${v.title}\n   URL: ${v.videoURL}\n   Quality: ${v.quality}\n   Type: ${v.type}\n   Method: ${v.method || 'unknown'}\n   Page: ${v.pageURL}\n`
1145            ).join('\n');
1146            filename = `adult-time-urls-${Date.now()}.txt`;
1147            mimeType = 'text/plain';
1148        } else if (format === 'json') {
1149            content = JSON.stringify(state.extractedURLs, null, 2);
1150            filename = `adult-time-urls-${Date.now()}.json`;
1151            mimeType = 'application/json';
1152        }
1153
1154        const blob = new Blob([content], { type: mimeType });
1155        const url = URL.createObjectURL(blob);
1156        const a = document.createElement('a');
1157        a.href = url;
1158        a.download = filename;
1159        a.click();
1160        URL.revokeObjectURL(url);
1161
1162        updateStatus(`Exported ${state.extractedURLs.length} URLs as ${format.toUpperCase()}!`, 'success');
1163    }
1164
1165    async function clearURLs() {
1166        if (!confirm('Are you sure you want to clear all extracted URLs? This cannot be undone.')) {
1167            return;
1168        }
1169
1170        await clearState();
1171        updateUI();
1172        updateStatus('All URLs cleared', 'info');
1173    }
1174
1175    // ============================================
1176    // PAGE DETECTION AND AUTO-PROCESSING
1177    // ============================================
1178
1179    function detectPageType() {
1180        const url = window.location.href;
1181        
1182        if (url.includes('/videos') || document.querySelectorAll('a.SceneThumb-SceneImageLink-Link[href*="/video/"]').length > 0) {
1183            return 'listing';
1184        } else if (url.includes('/video/') && document.querySelector('button.ScenePlayerHeaderPlus-WatchEpisodeButton-Button')) {
1185            return 'video';
1186        }
1187        
1188        return 'unknown';
1189    }
1190
1191    // ============================================
1192    // INITIALIZATION
1193    // ============================================
1194
1195    async function init() {
1196        console.log('[Init] Adult Time Video URL Extractor initialized');
1197        
1198        // Setup network interceptor first
1199        setupNetworkInterceptor();
1200        
1201        // Load saved state
1202        await loadState();
1203        
1204        // Detect page type
1205        const pageType = detectPageType();
1206        console.log(`[Init] Page type detected: ${pageType}`);
1207        
1208        // Create UI
1209        createControlPanel();
1210        
1211        // Update UI with loaded state
1212        updateUI();
1213        
1214        // If we're on a video page and extraction is running, process it
1215        if (pageType === 'video' && state.isRunning && !state.isPaused) {
1216            console.log('[Init] On video page with active extraction, processing...');
1217            await sleep(1000);
1218            await processCurrentVideoPage();
1219        }
1220        
1221        console.log('[Init] Extension ready!');
1222    }
1223
1224    // Wait for page to load
1225    if (document.readyState === 'loading') {
1226        document.addEventListener('DOMContentLoaded', init);
1227    } else {
1228        init();
1229    }
1230
1231})();
Adult Time Video URL Extractor & Downloader | Robomonkey