Adult Time Video URL Extractor & Downloader

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

Size

56.5 KB

Version

1.1.12

Created

Mar 19, 2026

Updated

29 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.12
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                    <button id="vex-quick-extract" class="vex-btn vex-btn-success" style="width:100%;">⚡ Quick Extract (Current Video)</button>
172                </div>
173
174                <div class="vex-progress-section">
175                    <div class="vex-progress-info">
176                        <span id="vex-status">Ready</span>
177                        <span id="vex-progress-text">0/0</span>
178                    </div>
179                    <div class="vex-progress-bar">
180                        <div id="vex-progress-fill"></div>
181                    </div>
182                    <div id="vex-current-video" class="vex-current-video"></div>
183                </div>
184
185                <div class="vex-results-section">
186                    <div class="vex-results-header">
187                        <h4>Extracted URLs (<span id="vex-url-count">0</span>)</h4>
188                        <div class="vex-results-actions">
189                            <button id="vex-copy-all" class="vex-btn vex-btn-small" disabled>📋 Copy All</button>
190                            <button id="vex-copy-m3u8" class="vex-btn vex-btn-small" disabled>📋 Copy M3U8</button>
191                            <button id="vex-export-txt" class="vex-btn vex-btn-small" disabled>💾 Export TXT</button>
192                            <button id="vex-export-json" class="vex-btn vex-btn-small" disabled>💾 Export JSON</button>
193                            <button id="vex-clear-urls" class="vex-btn vex-btn-small vex-btn-danger" disabled>🗑 Clear</button>
194                        </div>
195                    </div>
196                    <div id="vex-url-list" class="vex-url-list"></div>
197                </div>
198
199                <div class="vex-stats">
200                    <div class="vex-stat">
201                        <span class="vex-stat-label">Success:</span>
202                        <span id="vex-stat-success" class="vex-stat-value">0</span>
203                    </div>
204                    <div class="vex-stat">
205                        <span class="vex-stat-label">Failed:</span>
206                        <span id="vex-stat-failed" class="vex-stat-value">0</span>
207                    </div>
208                    <div class="vex-stat">
209                        <span class="vex-stat-label">Current Page:</span>
210                        <span id="vex-stat-page" class="vex-stat-value">1</span>
211                    </div>
212                </div>
213            </div>
214        `;
215
216        document.body.appendChild(panel);
217        addStyles();
218        attachEventListeners();
219    }
220
221    function addStyles() {
222        if (document.getElementById('vex-styles')) return;
223        
224        const styleSheet = document.createElement('style');
225        styleSheet.id = 'vex-styles';
226        styleSheet.textContent = `
227            #video-url-extractor-panel {
228                position: fixed;
229                top: 20px;
230                right: 20px;
231                width: 450px;
232                max-height: 90vh;
233                background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
234                border: 2px solid #444;
235                border-radius: 12px;
236                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
237                z-index: 999999;
238                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
239                color: #fff;
240                overflow: hidden;
241                backdrop-filter: blur(10px);
242            }
243
244            .vex-header {
245                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
246                padding: 15px 20px;
247                display: flex;
248                justify-content: space-between;
249                align-items: center;
250                cursor: move;
251                user-select: none;
252            }
253
254            .vex-header h3 {
255                margin: 0;
256                font-size: 18px;
257                font-weight: 600;
258            }
259
260            #vex-minimize {
261                background: rgba(255, 255, 255, 0.2);
262                border: none;
263                color: white;
264                width: 30px;
265                height: 30px;
266                border-radius: 50%;
267                cursor: pointer;
268                font-size: 20px;
269                display: flex;
270                align-items: center;
271                justify-content: center;
272                transition: all 0.3s;
273            }
274
275            #vex-minimize:hover {
276                background: rgba(255, 255, 255, 0.3);
277                transform: scale(1.1);
278            }
279
280            .vex-content {
281                padding: 20px;
282                max-height: calc(90vh - 70px);
283                overflow-y: auto;
284            }
285
286            .vex-content::-webkit-scrollbar {
287                width: 8px;
288            }
289
290            .vex-content::-webkit-scrollbar-track {
291                background: #2d2d2d;
292            }
293
294            .vex-content::-webkit-scrollbar-thumb {
295                background: #667eea;
296                border-radius: 4px;
297            }
298
299            .vex-section {
300                margin-bottom: 15px;
301            }
302
303            .vex-section label {
304                display: block;
305                margin-bottom: 5px;
306                font-size: 13px;
307                color: #ccc;
308            }
309
310            .vex-section input[type="number"],
311            .vex-section select {
312                width: 100%;
313                padding: 10px;
314                background: #3a3a3a;
315                border: 1px solid #555;
316                border-radius: 6px;
317                color: #fff;
318                font-size: 14px;
319                transition: all 0.3s;
320            }
321
322            .vex-section input[type="number"]:focus,
323            .vex-section select:focus {
324                outline: none;
325                border-color: #667eea;
326                box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
327            }
328
329            .vex-section input[type="checkbox"] {
330                margin-right: 8px;
331                cursor: pointer;
332            }
333
334            .vex-controls {
335                display: flex;
336                gap: 8px;
337                margin: 20px 0;
338                flex-wrap: wrap;
339            }
340
341            .vex-btn {
342                flex: 1;
343                padding: 10px 15px;
344                border: none;
345                border-radius: 6px;
346                cursor: pointer;
347                font-size: 13px;
348                font-weight: 600;
349                transition: all 0.3s;
350                min-width: 80px;
351            }
352
353            .vex-btn:disabled {
354                opacity: 0.5;
355                cursor: not-allowed;
356            }
357
358            .vex-btn-primary {
359                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
360                color: white;
361            }
362
363            .vex-btn-primary:hover:not(:disabled) {
364                transform: translateY(-2px);
365                box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
366            }
367
368            .vex-btn-warning {
369                background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
370                color: white;
371            }
372
373            .vex-btn-danger {
374                background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
375                color: white;
376            }
377
378            .vex-btn-success {
379                background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
380                color: white;
381            }
382
383            .vex-btn-small {
384                padding: 6px 10px;
385                font-size: 11px;
386                min-width: auto;
387            }
388
389            .vex-progress-section {
390                background: #2a2a2a;
391                padding: 15px;
392                border-radius: 8px;
393                margin: 15px 0;
394            }
395
396            .vex-progress-info {
397                display: flex;
398                justify-content: space-between;
399                margin-bottom: 8px;
400                font-size: 13px;
401            }
402
403            #vex-status {
404                color: #4facfe;
405                font-weight: 600;
406            }
407
408            .vex-progress-bar {
409                height: 8px;
410                background: #3a3a3a;
411                border-radius: 4px;
412                overflow: hidden;
413            }
414
415            #vex-progress-fill {
416                height: 100%;
417                background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
418                width: 0%;
419                transition: width 0.3s;
420            }
421
422            .vex-current-video {
423                margin-top: 10px;
424                font-size: 12px;
425                color: #aaa;
426                font-style: italic;
427                min-height: 18px;
428            }
429
430            .vex-results-section {
431                margin: 20px 0;
432            }
433
434            .vex-results-header {
435                display: flex;
436                justify-content: space-between;
437                align-items: center;
438                margin-bottom: 10px;
439                flex-wrap: wrap;
440                gap: 10px;
441            }
442
443            .vex-results-header h4 {
444                margin: 0;
445                font-size: 15px;
446            }
447
448            .vex-results-actions {
449                display: flex;
450                gap: 5px;
451                flex-wrap: wrap;
452            }
453
454            .vex-url-list {
455                background: #2a2a2a;
456                border-radius: 8px;
457                max-height: 300px;
458                overflow-y: auto;
459                padding: 10px;
460            }
461
462            .vex-url-list::-webkit-scrollbar {
463                width: 6px;
464            }
465
466            .vex-url-list::-webkit-scrollbar-thumb {
467                background: #667eea;
468                border-radius: 3px;
469            }
470
471            .vex-url-item {
472                background: #3a3a3a;
473                padding: 10px;
474                margin-bottom: 8px;
475                border-radius: 6px;
476                font-size: 12px;
477                border-left: 3px solid #667eea;
478            }
479
480            .vex-url-item-title {
481                font-weight: 600;
482                color: #fff;
483                margin-bottom: 5px;
484                display: flex;
485                justify-content: space-between;
486                align-items: center;
487            }
488
489            .vex-url-item-quality {
490                background: #667eea;
491                padding: 2px 8px;
492                border-radius: 4px;
493                font-size: 10px;
494                font-weight: 600;
495            }
496
497            .vex-url-item-url {
498                color: #4facfe;
499                word-break: break-all;
500                font-family: monospace;
501                font-size: 11px;
502            }
503
504            .vex-url-item-actions {
505                margin-top: 8px;
506                display: flex;
507                gap: 5px;
508            }
509
510            .vex-btn-small {
511                padding: 6px 10px;
512                font-size: 11px;
513                min-width: auto;
514            }
515
516            .vex-btn-primary {
517                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
518                color: white;
519            }
520
521            .vex-btn-primary:hover:not(:disabled) {
522                transform: translateY(-2px);
523                box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
524            }
525
526            .vex-btn-warning {
527                background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
528                color: white;
529            }
530
531            .vex-btn-danger {
532                background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
533                color: white;
534            }
535
536            .vex-btn-success {
537                background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
538                color: white;
539            }
540
541            .vex-btn-small {
542                padding: 6px 10px;
543                font-size: 11px;
544                min-width: auto;
545            }
546
547            .vex-pulse {
548                animation: pulse 2s infinite;
549            }
550
551            @keyframes pulse {
552                0%, 100% { opacity: 1; }
553                50% { opacity: 0.5; }
554            }
555        `;
556        document.head.appendChild(styleSheet);
557    }
558
559    function attachEventListeners() {
560        // Minimize/Maximize
561        document.getElementById('vex-minimize').addEventListener('click', () => {
562            const panel = document.getElementById('video-url-extractor-panel');
563            panel.classList.toggle('vex-minimized');
564            const btn = document.getElementById('vex-minimize');
565            btn.textContent = panel.classList.contains('vex-minimized') ? '+' : '−';
566        });
567
568        // Make panel draggable
569        makeDraggable(document.getElementById('video-url-extractor-panel'));
570
571        // Control buttons
572        document.getElementById('vex-start').addEventListener('click', startExtraction);
573        document.getElementById('vex-pause').addEventListener('click', pauseExtraction);
574        document.getElementById('vex-stop').addEventListener('click', stopExtraction);
575        document.getElementById('vex-resume').addEventListener('click', resumeExtraction);
576        document.getElementById('vex-quick-extract').addEventListener('click', quickExtractCurrentVideo);
577
578        // Export buttons
579        document.getElementById('vex-copy-all').addEventListener('click', () => copyURLs('all'));
580        document.getElementById('vex-copy-m3u8').addEventListener('click', () => copyURLs('m3u8'));
581        document.getElementById('vex-export-txt').addEventListener('click', () => exportURLs('txt'));
582        document.getElementById('vex-export-json').addEventListener('click', () => exportURLs('json'));
583        document.getElementById('vex-clear-urls').addEventListener('click', clearURLs);
584    }
585
586    function makeDraggable(element) {
587        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
588        const header = element.querySelector('.vex-header');
589        
590        header.onmousedown = dragMouseDown;
591
592        function dragMouseDown(e) {
593            e.preventDefault();
594            pos3 = e.clientX;
595            pos4 = e.clientY;
596            document.onmouseup = closeDragElement;
597            document.onmousemove = elementDrag;
598        }
599
600        function elementDrag(e) {
601            e.preventDefault();
602            pos1 = pos3 - e.clientX;
603            pos2 = pos4 - e.clientY;
604            pos3 = e.clientX;
605            pos4 = e.clientY;
606            element.style.top = (element.offsetTop - pos2) + 'px';
607            element.style.left = (element.offsetLeft - pos1) + 'px';
608            element.style.right = 'auto';
609        }
610
611        function closeDragElement() {
612            document.onmouseup = null;
613            document.onmousemove = null;
614        }
615    }
616
617    // ============================================
618    // EXTRACTION LOGIC - IN-PAGE METHOD
619    // ============================================
620
621    async function startExtraction() {
622        const videoCount = parseInt(document.getElementById('vex-video-count').value);
623        if (videoCount < 1) {
624            alert('Please enter a valid number of videos (minimum 1)');
625            return;
626        }
627
628        // Check if we're on a video listing page
629        const isListingPage = window.location.href.includes('/videos') || 
630                              document.querySelectorAll('a.SceneThumb-SceneImageLink-Link[href*="/video/"]').length > 0;
631
632        if (!isListingPage) {
633            alert('Please start from a video listing page (e.g., /videos)');
634            return;
635        }
636
637        // Build video queue from current page
638        const videoLinks = getVideoLinksOnPage();
639        state.videoQueue = videoLinks.slice(0, videoCount).map(link => ({
640            url: link.href,
641            title: link.getAttribute('title') || 'Unknown Video'
642        }));
643
644        state.isRunning = true;
645        state.isPaused = false;
646        state.totalVideosToProcess = Math.min(videoCount, state.videoQueue.length);
647        state.processedVideos = 0;
648        state.currentVideoIndex = 0;
649        state.currentPage = 1;
650        state.returnURL = window.location.href;
651
652        await saveState();
653        updateUI();
654        updateStatus('Starting extraction...', 'info');
655
656        document.getElementById('vex-start').disabled = true;
657        document.getElementById('vex-pause').disabled = false;
658        document.getElementById('vex-stop').disabled = false;
659
660        console.log(`[Extractor] Starting extraction of ${state.totalVideosToProcess} videos`);
661        console.log('[Extractor] Video queue:', state.videoQueue);
662
663        // Navigate to first video
664        if (state.videoQueue.length > 0) {
665            const firstVideo = state.videoQueue[0];
666            console.log(`[Extractor] Navigating to first video: ${firstVideo.title}`);
667            window.location.href = firstVideo.url;
668        }
669    }
670
671    async function pauseExtraction() {
672        state.isPaused = true;
673        await saveState();
674        updateStatus('Paused', 'warning');
675        
676        document.getElementById('vex-pause').style.display = 'none';
677        document.getElementById('vex-resume').style.display = 'block';
678    }
679
680    async function resumeExtraction() {
681        state.isPaused = false;
682        await saveState();
683        updateStatus('Resuming...', 'info');
684        
685        document.getElementById('vex-pause').style.display = 'block';
686        document.getElementById('vex-resume').style.display = 'none';
687
688        // Continue processing
689        await processCurrentVideoPage();
690    }
691
692    async function stopExtraction() {
693        state.isRunning = false;
694        state.isPaused = false;
695        await saveState();
696        updateStatus('Stopped', 'danger');
697        
698        document.getElementById('vex-start').disabled = false;
699        document.getElementById('vex-pause').disabled = true;
700        document.getElementById('vex-stop').disabled = true;
701        document.getElementById('vex-resume').style.display = 'none';
702
703        // Return to listing page
704        if (state.returnURL && !window.location.href.includes('/videos')) {
705            console.log('[Extractor] Returning to listing page...');
706            window.location.href = state.returnURL;
707        }
708    }
709
710    function getVideoLinksOnPage() {
711        const links = document.querySelectorAll('a.SceneThumb-SceneImageLink-Link[href*="/video/"]');
712        return Array.from(links);
713    }
714
715    // ============================================
716    // VIDEO PAGE PROCESSING
717    // ============================================
718
719    async function processCurrentVideoPage() {
720        if (!state.isRunning || state.isPaused) {
721            console.log('[Processor] Not running or paused, skipping');
722            return;
723        }
724
725        const currentVideo = state.videoQueue[state.currentVideoIndex];
726        if (!currentVideo) {
727            console.log('[Processor] No current video, finishing');
728            await finishExtraction();
729            return;
730        }
731
732        console.log(`[Processor] Processing video ${state.currentVideoIndex + 1}/${state.totalVideosToProcess}: ${currentVideo.title}`);
733        updateCurrentVideo(currentVideo.title);
734        updateStatus(`Processing ${state.currentVideoIndex + 1}/${state.totalVideosToProcess}`, 'info');
735
736        // Start capturing network requests
737        capturedURLs = [];
738        isCapturing = true;
739        console.log('[Processor] Started capturing network requests');
740
741        // Wait for page to be fully loaded
742        await sleep(2000);
743
744        // Try to find and click the Watch Episode button
745        const watchButton = document.querySelector('button.ScenePlayerHeaderPlus-WatchEpisodeButton-Button');
746        
747        if (watchButton) {
748            console.log('[Processor] Found Watch Episode button, clicking...');
749            watchButton.click();
750            
751            // Wait for video to start loading
752            await sleep(5000);
753            
754            // Stop capturing
755            isCapturing = false;
756            console.log(`[Processor] Stopped capturing. Found ${capturedURLs.length} URLs`);
757            
758            // Also check Performance API for additional URLs
759            const performanceURLs = getPerformanceVideoURLs();
760            console.log(`[Processor] Found ${performanceURLs.length} URLs from Performance API`);
761            
762            // Combine all captured URLs
763            const allCapturedURLs = [...capturedURLs, ...performanceURLs];
764            console.log(`[Processor] Total captured URLs: ${allCapturedURLs.length}`);
765            
766            if (allCapturedURLs.length > 0) {
767                // Process and select best URL
768                const videoData = selectBestCapturedURL(allCapturedURLs, currentVideo);
769                
770                if (videoData) {
771                    state.extractedURLs.push(videoData);
772                    state.processedVideos++;
773                    console.log(`[Processor] ✓ Successfully extracted URL for: ${currentVideo.title}`);
774                } else {
775                    state.failedVideos.push(currentVideo);
776                    console.warn(`[Processor] ✗ Failed to extract URL for: ${currentVideo.title}`);
777                }
778            } else {
779                state.failedVideos.push(currentVideo);
780                console.warn(`[Processor] ✗ No URLs captured for: ${currentVideo.title}`);
781            }
782        } else {
783            console.warn('[Processor] Watch Episode button not found');
784            state.failedVideos.push(currentVideo);
785        }
786
787        // Update UI
788        updateProgress();
789        updateStats();
790        updateURLList();
791        await saveState();
792
793        // Move to next video
794        state.currentVideoIndex++;
795        
796        if (state.currentVideoIndex < state.videoQueue.length && state.processedVideos < state.totalVideosToProcess) {
797            const nextVideo = state.videoQueue[state.currentVideoIndex];
798            console.log(`[Processor] Moving to next video: ${nextVideo.title}`);
799            await sleep(1000);
800            window.location.href = nextVideo.url;
801        } else {
802            await finishExtraction();
803        }
804    }
805
806    function getPerformanceVideoURLs() {
807        const entries = performance.getEntriesByType('resource');
808        const videoURLs = [];
809        
810        entries.forEach(entry => {
811            const url = entry.name;
812            if (url.includes('.m3u8') || url.includes('.mp4') || url.includes('.mpd') ||
813                url.includes('streaming-hls.gammacdn.com') || url.includes('m3u8.gammacdn.com')) {
814                videoURLs.push({ url, type: 'performance', timestamp: Date.now() });
815                console.log('[Performance] Found URL:', url.substring(0, 100) + '...');
816            }
817        });
818        
819        return videoURLs;
820    }
821
822    function selectBestCapturedURL(capturedURLs, videoInfo) {
823        const preferredQuality = document.getElementById('vex-quality').value;
824        
825        console.log('[Selector] Processing captured URLs...');
826        
827        // Parse and categorize URLs
828        const sources = [];
829        
830        capturedURLs.forEach(item => {
831            let url = item.url;
832            
833            // Decode if it's an encoded gammacdn URL
834            if (url.includes('m3u8.gammacdn.com/?u=')) {
835                try {
836                    const match = url.match(/\?u=([^&]+)/);
837                    if (match) {
838                        const decoded = decodeURIComponent(match[1]);
839                        console.log('[Selector] Decoded URL:', decoded.substring(0, 100) + '...');
840                        url = decoded;
841                    }
842                } catch (e) {
843                    console.warn('[Selector] Failed to decode URL:', e.message);
844                }
845            }
846            
847            // Skip .ts segment files
848            if (url.includes('.ts?') || url.endsWith('.ts')) {
849                return;
850            }
851            
852            // Only keep master m3u8 files (not quality-specific segments)
853            if (url.includes('.m3u8')) {
854                const quality = extractQualityFromURL(url);
855                sources.push({
856                    url: url,
857                    type: 'm3u8',
858                    quality: quality,
859                    method: item.type
860                });
861                console.log(`[Selector] Added m3u8 source: ${quality} - ${url.substring(0, 80)}...`);
862            } else if (url.includes('.mp4')) {
863                const quality = extractQualityFromURL(url);
864                sources.push({
865                    url: url,
866                    type: 'mp4',
867                    quality: quality,
868                    method: item.type
869                });
870                console.log(`[Selector] Added mp4 source: ${quality} - ${url.substring(0, 80)}...`);
871            } else if (url.includes('.mpd')) {
872                const quality = extractQualityFromURL(url);
873                sources.push({
874                    url: url,
875                    type: 'dash',
876                    quality: quality,
877                    method: item.type
878                });
879                console.log(`[Selector] Added dash source: ${quality} - ${url.substring(0, 80)}...`);
880            }
881        });
882
883        // Remove duplicates
884        const uniqueSources = sources.filter((source, index, self) =>
885            index === self.findIndex(s => s.url === source.url)
886        );
887
888        console.log(`[Selector] Found ${uniqueSources.length} unique sources`);
889
890        if (uniqueSources.length === 0) {
891            return null;
892        }
893
894        // Select best source
895        const selectedSource = selectBestSource(uniqueSources, preferredQuality);
896        console.log(`[Selector] Selected: ${selectedSource.quality} ${selectedSource.type}`);
897
898        return {
899            title: videoInfo.title,
900            pageURL: videoInfo.url,
901            videoURL: selectedSource.url,
902            type: selectedSource.type,
903            quality: selectedSource.quality,
904            method: selectedSource.method,
905            allSources: uniqueSources,
906            timestamp: new Date().toISOString()
907        };
908    }
909
910    function extractQualityFromURL(url) {
911        // Try to extract quality from URL
912        const qualityMatch = url.match(/[_-](\d{3,4})[pP]/);
913        if (qualityMatch) {
914            return qualityMatch[1] + 'p';
915        }
916        
917        // Check for quality indicators in URL
918        if (url.includes('2160') || url.includes('4k') || url.includes('uhd')) return '2160p';
919        if (url.includes('1080') || url.includes('fhd')) return '1080p';
920        if (url.includes('720') || url.includes('hd')) return '720p';
921        if (url.includes('576')) return '576p';
922        if (url.includes('480') || url.includes('sd')) return '480p';
923        if (url.includes('360')) return '360p';
924        if (url.includes('240')) return '240p';
925        
926        return 'unknown';
927    }
928
929    function selectBestSource(sources, preferredQuality) {
930        console.log(`[Selector] Selecting best source from ${sources.length} options, preferred quality: ${preferredQuality}`);
931        
932        // Prioritize by type: m3u8 > mp4 > dash > others
933        const m3u8Sources = sources.filter(s => s.type === 'm3u8');
934        const mp4Sources = sources.filter(s => s.type === 'mp4');
935        const dashSources = sources.filter(s => s.type === 'dash');
936        const otherSources = sources.filter(s => !['m3u8', 'mp4', 'dash'].includes(s.type));
937
938        let prioritizedSources = [...m3u8Sources, ...mp4Sources, ...dashSources, ...otherSources];
939
940        if (preferredQuality === 'highest') {
941            // Find highest quality
942            const qualityOrder = ['2160p', '1080p', '720p', '576p', '480p', '360p', '240p', 'unknown'];
943            for (const quality of qualityOrder) {
944                const found = prioritizedSources.find(s => s.quality === quality);
945                if (found) {
946                    console.log(`[Selector] Selected highest quality: ${quality}`);
947                    return found;
948                }
949            }
950        } else {
951            // Find preferred quality or closest
952            const targetQuality = preferredQuality + 'p';
953            const found = prioritizedSources.find(s => s.quality === targetQuality);
954            if (found) {
955                console.log(`[Selector] Found exact match for ${targetQuality}`);
956                return found;
957            }
958
959            // Find closest quality (prefer higher)
960            const qualityValue = parseInt(preferredQuality);
961            const sorted = prioritizedSources
962                .filter(s => s.quality !== 'unknown')
963                .sort((a, b) => {
964                    const aVal = parseInt(a.quality);
965                    const bVal = parseInt(b.quality);
966                    const aDiff = Math.abs(aVal - qualityValue);
967                    const bDiff = Math.abs(bVal - qualityValue);
968                    if (aDiff === bDiff) return bVal - aVal; // Prefer higher
969                    return aDiff - bDiff;
970                });
971            
972            if (sorted.length > 0) {
973                console.log(`[Selector] Selected closest quality: ${sorted[0].quality}`);
974                return sorted[0];
975            }
976        }
977
978        // Fallback to first available
979        console.log('[Selector] Using fallback: first available source');
980        return prioritizedSources[0] || sources[0];
981    }
982
983    async function finishExtraction() {
984        console.log('[Extractor] Finishing extraction...');
985        
986        state.isRunning = false;
987        updateStatus('Extraction complete!', 'success');
988        
989        document.getElementById('vex-start').disabled = false;
990        document.getElementById('vex-pause').disabled = true;
991        document.getElementById('vex-stop').disabled = true;
992        document.getElementById('vex-resume').style.display = 'none';
993
994        await saveState();
995
996        // Return to listing page
997        if (state.returnURL && !window.location.href.includes('/videos')) {
998            console.log('[Extractor] Returning to listing page...');
999            await sleep(2000);
1000            window.location.href = state.returnURL;
1001        }
1002    }
1003
1004    // ============================================
1005    // QUICK EXTRACT FUNCTION
1006    // ============================================
1007
1008    async function quickExtractCurrentVideo() {
1009        console.log('[Quick Extract] Starting quick extraction for current video...');
1010        
1011        // Check if we're on a video page
1012        if (!window.location.href.includes('/video/')) {
1013            alert('Please navigate to a video page first!');
1014            return;
1015        }
1016
1017        updateStatus('Quick extracting...', 'info');
1018        
1019        // Get video title from page
1020        const titleEl = document.querySelector('h1.ScenePlayerHeaderPlus-SceneTitle-Title');
1021        const videoTitle = titleEl ? titleEl.textContent.trim() : 'Current Video';
1022        
1023        updateCurrentVideo(videoTitle);
1024        
1025        // Start capturing
1026        capturedURLs = [];
1027        isCapturing = true;
1028        console.log('[Quick Extract] Started capturing network requests');
1029        
1030        // Check if video is already playing
1031        const videoEl = document.querySelector('video.vjs-tech');
1032        const isPlaying = videoEl && !videoEl.paused;
1033        
1034        if (!isPlaying) {
1035            // Try to find and click the Watch Episode button
1036            const watchButton = document.querySelector('button.ScenePlayerHeaderPlus-WatchEpisodeButton-Button');
1037            
1038            if (watchButton) {
1039                console.log('[Quick Extract] Clicking Watch Episode button...');
1040                watchButton.click();
1041                await sleep(5000);
1042            } else {
1043                console.log('[Quick Extract] Watch button not found, checking for existing video...');
1044                await sleep(2000);
1045            }
1046        } else {
1047            console.log('[Quick Extract] Video already playing, capturing URLs...');
1048            await sleep(3000);
1049        }
1050        
1051        // Stop capturing
1052        isCapturing = false;
1053        console.log(`[Quick Extract] Stopped capturing. Found ${capturedURLs.length} URLs`);
1054        
1055        // Also check Performance API
1056        const performanceURLs = getPerformanceVideoURLs();
1057        console.log(`[Quick Extract] Found ${performanceURLs.length} URLs from Performance API`);
1058        
1059        // Combine all captured URLs
1060        const allCapturedURLs = [...capturedURLs, ...performanceURLs];
1061        console.log(`[Quick Extract] Total captured URLs: ${allCapturedURLs.length}`);
1062        
1063        if (allCapturedURLs.length > 0) {
1064            const videoInfo = {
1065                url: window.location.href,
1066                title: videoTitle
1067            };
1068            
1069            const videoData = selectBestCapturedURL(allCapturedURLs, videoInfo);
1070            
1071            if (videoData) {
1072                // Check if this video is already in the list
1073                const existingIndex = state.extractedURLs.findIndex(v => v.pageURL === videoInfo.url);
1074                
1075                if (existingIndex >= 0) {
1076                    // Update existing entry
1077                    state.extractedURLs[existingIndex] = videoData;
1078                    console.log('[Quick Extract] Updated existing video entry');
1079                } else {
1080                    // Add new entry
1081                    state.extractedURLs.push(videoData);
1082                    console.log('[Quick Extract] Added new video entry');
1083                }
1084                
1085                await saveState();
1086                updateUI();
1087                updateStatus('✓ Video URL extracted successfully!', 'success');
1088                
1089                // Copy URL to clipboard automatically
1090                await GM.setClipboard(videoData.videoURL);
1091                console.log('[Quick Extract] URL copied to clipboard');
1092                
1093                alert(`Video URL extracted and copied to clipboard!\n\nTitle: ${videoTitle}\nQuality: ${videoData.quality}\nType: ${videoData.type.toUpperCase()}\n\nClick the "📥 Direct Download" button to download the video directly!`);
1094            } else {
1095                updateStatus('✗ Failed to extract URL', 'danger');
1096                alert('Failed to extract video URL. No valid video sources found.');
1097            }
1098        } else {
1099            updateStatus('✗ No URLs captured', 'danger');
1100            alert('No video URLs were captured. Try playing the video first, then click Quick Extract again.');
1101        }
1102    }
1103
1104    // ============================================
1105    // DIRECT DOWNLOAD FUNCTION
1106    // ============================================
1107
1108    async function directDownloadVideo(videoURL, videoTitle) {
1109        console.log('[Direct Download] Starting direct download...');
1110        updateStatus('Downloading video...', 'info');
1111        
1112        try {
1113            // Show download progress dialog
1114            const progressDialog = createDownloadProgressDialog(videoTitle);
1115            document.body.appendChild(progressDialog);
1116            
1117            // Fetch the m3u8 playlist
1118            updateDownloadProgress('Fetching video playlist...', 0);
1119            const playlistResponse = await GM.xmlhttpRequest({
1120                method: 'GET',
1121                url: videoURL,
1122                responseType: 'text'
1123            });
1124            
1125            const playlistContent = playlistResponse.responseText;
1126            console.log('[Direct Download] Playlist fetched');
1127            
1128            // Parse m3u8 to get segment URLs
1129            const segments = parseM3U8Playlist(playlistContent, videoURL);
1130            console.log(`[Direct Download] Found ${segments.length} segments`);
1131            
1132            if (segments.length === 0) {
1133                throw new Error('No video segments found in playlist');
1134            }
1135            
1136            // Download all segments
1137            updateDownloadProgress(`Downloading ${segments.length} segments...`, 0);
1138            const segmentBlobs = [];
1139            
1140            for (let i = 0; i < segments.length; i++) {
1141                const segmentURL = segments[i];
1142                console.log(`[Direct Download] Downloading segment ${i + 1}/${segments.length}`);
1143                
1144                const segmentResponse = await GM.xmlhttpRequest({
1145                    method: 'GET',
1146                    url: segmentURL,
1147                    responseType: 'blob'
1148                });
1149                
1150                segmentBlobs.push(segmentResponse.response);
1151                
1152                const progress = ((i + 1) / segments.length) * 100;
1153                updateDownloadProgress(`Downloaded ${i + 1}/${segments.length} segments`, progress);
1154            }
1155            
1156            // Combine all segments into one blob
1157            updateDownloadProgress('Combining video segments...', 100);
1158            const videoBlob = new Blob(segmentBlobs, { type: 'video/mp2t' });
1159            
1160            // Create download link
1161            const downloadURL = URL.createObjectURL(videoBlob);
1162            const a = document.createElement('a');
1163            a.href = downloadURL;
1164            a.download = `${videoTitle.replace(/[^a-z0-9]/gi, '_')}.ts`;
1165            a.click();
1166            
1167            URL.revokeObjectURL(downloadURL);
1168            
1169            // Close progress dialog
1170            progressDialog.remove();
1171            
1172            updateStatus('✓ Video downloaded successfully!', 'success');
1173            alert(`Video downloaded successfully!\n\nFile: ${videoTitle}.ts\n\nNote: The file is in .ts format. You can convert it to .mp4 using VLC or FFmpeg if needed.`);
1174            
1175        } catch (error) {
1176            console.error('[Direct Download] Error:', error);
1177            updateStatus('✗ Download failed', 'danger');
1178            alert(`Download failed: ${error.message}\n\nPlease try using yt-dlp or FFmpeg instead.`);
1179            
1180            // Remove progress dialog if it exists
1181            const progressDialog = document.getElementById('vex-download-progress');
1182            if (progressDialog) progressDialog.remove();
1183        }
1184    }
1185
1186    function parseM3U8Playlist(playlistContent, baseURL) {
1187        const lines = playlistContent.split('\n');
1188        const segments = [];
1189        const baseURLObj = new URL(baseURL);
1190        const basePath = baseURLObj.origin + baseURLObj.pathname.substring(0, baseURLObj.pathname.lastIndexOf('/') + 1);
1191        
1192        for (const line of lines) {
1193            const trimmedLine = line.trim();
1194            
1195            // Skip comments and empty lines
1196            if (trimmedLine.startsWith('#') || trimmedLine === '') {
1197                continue;
1198            }
1199            
1200            // Check if it's another m3u8 (quality variant)
1201            if (trimmedLine.endsWith('.m3u8')) {
1202                // This is a master playlist, we need to fetch the variant
1203                const variantURL = trimmedLine.startsWith('http') ? trimmedLine : basePath + trimmedLine;
1204                console.log('[Parser] Found variant playlist:', variantURL);
1205                // For now, we'll just return this URL and let the user handle it
1206                // In a full implementation, we'd recursively fetch this
1207                continue;
1208            }
1209            
1210            // It's a segment URL
1211            if (trimmedLine.endsWith('.ts') || trimmedLine.includes('.ts?')) {
1212                const segmentURL = trimmedLine.startsWith('http') ? trimmedLine : basePath + trimmedLine;
1213                segments.push(segmentURL);
1214            }
1215        }
1216        
1217        return segments;
1218    }
1219
1220    function createDownloadProgressDialog(videoTitle) {
1221        const dialog = document.createElement('div');
1222        dialog.id = 'vex-download-progress';
1223        dialog.innerHTML = `
1224            <div class="vex-download-overlay"></div>
1225            <div class="vex-download-dialog">
1226                <h3>📥 Downloading Video</h3>
1227                <p class="vex-download-title">${videoTitle}</p>
1228                <div class="vex-download-status">Initializing...</div>
1229                <div class="vex-download-progress-bar">
1230                    <div class="vex-download-progress-fill" style="width: 0%"></div>
1231                </div>
1232                <div class="vex-download-percentage">0%</div>
1233            </div>
1234        `;
1235        
1236        // Add styles for download dialog
1237        const styles = `
1238            #vex-download-progress {
1239                position: fixed;
1240                top: 0;
1241                left: 0;
1242                width: 100%;
1243                height: 100%;
1244                z-index: 9999999;
1245                display: flex;
1246                align-items: center;
1247                justify-content: center;
1248            }
1249            
1250            .vex-download-overlay {
1251                position: absolute;
1252                top: 0;
1253                left: 0;
1254                width: 100%;
1255                height: 100%;
1256                background: rgba(0, 0, 0, 0.8);
1257                backdrop-filter: blur(5px);
1258            }
1259            
1260            .vex-download-dialog {
1261                position: relative;
1262                background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
1263                border: 2px solid #667eea;
1264                border-radius: 12px;
1265                padding: 30px;
1266                min-width: 400px;
1267                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1268                color: #fff;
1269                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1270            }
1271            
1272            .vex-download-dialog h3 {
1273                margin: 0 0 15px 0;
1274                font-size: 20px;
1275                color: #667eea;
1276            }
1277            
1278            .vex-download-title {
1279                margin: 0 0 20px 0;
1280                font-size: 14px;
1281                color: #aaa;
1282                font-style: italic;
1283            }
1284            
1285            .vex-download-status {
1286                margin-bottom: 10px;
1287                font-size: 14px;
1288                color: #4facfe;
1289            }
1290            
1291            .vex-download-progress-bar {
1292                height: 20px;
1293                background: #3a3a3a;
1294                border-radius: 10px;
1295                overflow: hidden;
1296                margin-bottom: 10px;
1297            }
1298            
1299            .vex-download-progress-fill {
1300                height: 100%;
1301                background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
1302                transition: width 0.3s;
1303            }
1304            
1305            .vex-download-percentage {
1306                text-align: center;
1307                font-size: 18px;
1308                font-weight: 600;
1309                color: #667eea;
1310            }
1311        `;
1312        
1313        const styleSheet = document.createElement('style');
1314        styleSheet.textContent = styles;
1315        dialog.appendChild(styleSheet);
1316        
1317        return dialog;
1318    }
1319
1320    function updateDownloadProgress(status, percentage) {
1321        const statusEl = document.querySelector('.vex-download-status');
1322        const fillEl = document.querySelector('.vex-download-progress-fill');
1323        const percentEl = document.querySelector('.vex-download-percentage');
1324        
1325        if (statusEl) statusEl.textContent = status;
1326        if (fillEl) fillEl.style.width = percentage + '%';
1327        if (percentEl) percentEl.textContent = Math.round(percentage) + '%';
1328    }
1329
1330    // ============================================
1331    // UI UPDATE FUNCTIONS
1332    // ============================================
1333
1334    function updateUI() {
1335        updateProgress();
1336        updateStats();
1337        updateURLList();
1338    }
1339
1340    function updateStatus(message) {
1341        const statusEl = document.getElementById('vex-status');
1342        if (statusEl) {
1343            statusEl.textContent = message;
1344            statusEl.className = 'vex-pulse';
1345            
1346            setTimeout(() => {
1347                statusEl.classList.remove('vex-pulse');
1348            }, 2000);
1349        }
1350    }
1351
1352    function updateProgress() {
1353        const progressText = document.getElementById('vex-progress-text');
1354        const progressFill = document.getElementById('vex-progress-fill');
1355        
1356        if (progressText) {
1357            progressText.textContent = `${state.processedVideos}/${state.totalVideosToProcess}`;
1358        }
1359        
1360        if (progressFill) {
1361            const percentage = state.totalVideosToProcess > 0 
1362                ? (state.processedVideos / state.totalVideosToProcess) * 100 
1363                : 0;
1364            progressFill.style.width = percentage + '%';
1365        }
1366    }
1367
1368    function updateCurrentVideo(title) {
1369        const currentVideoEl = document.getElementById('vex-current-video');
1370        if (currentVideoEl) {
1371            currentVideoEl.textContent = `Current: ${title}`;
1372        }
1373    }
1374
1375    function updateStats() {
1376        const successEl = document.getElementById('vex-stat-success');
1377        const failedEl = document.getElementById('vex-stat-failed');
1378        const pageEl = document.getElementById('vex-stat-page');
1379        
1380        if (successEl) successEl.textContent = state.extractedURLs.length;
1381        if (failedEl) failedEl.textContent = state.failedVideos.length;
1382        if (pageEl) pageEl.textContent = state.currentPage;
1383    }
1384
1385    function updateURLList() {
1386        const urlList = document.getElementById('vex-url-list');
1387        const urlCount = document.getElementById('vex-url-count');
1388        
1389        if (!urlList || !urlCount) return;
1390        
1391        urlCount.textContent = state.extractedURLs.length;
1392
1393        if (state.extractedURLs.length === 0) {
1394            urlList.innerHTML = '<div style="text-align: center; color: #888; padding: 20px;">No URLs extracted yet</div>';
1395            disableExportButtons();
1396            return;
1397        }
1398
1399        enableExportButtons();
1400
1401        urlList.innerHTML = state.extractedURLs.map((video, index) => `
1402            <div class="vex-url-item">
1403                <div class="vex-url-item-title">
1404                    <span>${index + 1}. ${video.title}</span>
1405                    <span class="vex-url-item-quality">${video.quality}${video.type.toUpperCase()}</span>
1406                </div>
1407                <div class="vex-url-item-url">${video.videoURL}</div>
1408                <div class="vex-url-item-actions">
1409                    <button class="vex-btn vex-btn-small vex-btn-primary" onclick="navigator.clipboard.writeText('${video.videoURL.replace(/'/g, '\\\'')}')">📋 Copy</button>
1410                    <button class="vex-btn vex-btn-small" onclick="window.open('${video.pageURL}', '_blank')">🔗 Open Page</button>
1411                    <button class="vex-btn vex-btn-small vex-btn-success" onclick="(${directDownloadVideo.toString()})('${video.videoURL.replace(/'/g, '\\\'')}', '${video.title.replace(/'/g, '\\\'')}')">📥 Direct Download</button>
1412                </div>
1413            </div>
1414        `).join('');
1415    }
1416
1417    function enableExportButtons() {
1418        const buttons = ['vex-copy-all', 'vex-copy-m3u8', 'vex-export-txt', 'vex-export-json', 'vex-clear-urls'];
1419        buttons.forEach(id => {
1420            const btn = document.getElementById(id);
1421            if (btn) btn.disabled = false;
1422        });
1423    }
1424
1425    function disableExportButtons() {
1426        const buttons = ['vex-copy-all', 'vex-copy-m3u8', 'vex-export-txt', 'vex-export-json', 'vex-clear-urls'];
1427        buttons.forEach(id => {
1428            const btn = document.getElementById(id);
1429            if (btn) btn.disabled = true;
1430        });
1431    }
1432
1433    // ============================================
1434    // EXPORT FUNCTIONS
1435    // ============================================
1436
1437    async function copyURLs(type) {
1438        let urls = [];
1439        
1440        if (type === 'all') {
1441            urls = state.extractedURLs.map(v => v.videoURL);
1442        } else if (type === 'm3u8') {
1443            urls = state.extractedURLs.filter(v => v.type === 'm3u8').map(v => v.videoURL);
1444        }
1445
1446        if (urls.length === 0) {
1447            alert('No URLs to copy!');
1448            return;
1449        }
1450
1451        const text = urls.join('\n');
1452        await GM.setClipboard(text);
1453        updateStatus(`Copied ${urls.length} URLs to clipboard!`, 'success');
1454    }
1455
1456    function exportURLs(format) {
1457        if (state.extractedURLs.length === 0) {
1458            alert('No URLs to export!');
1459            return;
1460        }
1461
1462        let content, filename, mimeType;
1463
1464        if (format === 'txt') {
1465            content = state.extractedURLs.map((v, i) => 
1466                `${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`
1467            ).join('\n');
1468            filename = `adult-time-urls-${Date.now()}.txt`;
1469            mimeType = 'text/plain';
1470        } else if (format === 'json') {
1471            content = JSON.stringify(state.extractedURLs, null, 2);
1472            filename = `adult-time-urls-${Date.now()}.json`;
1473            mimeType = 'application/json';
1474        }
1475
1476        const blob = new Blob([content], { type: mimeType });
1477        const url = URL.createObjectURL(blob);
1478        const a = document.createElement('a');
1479        a.href = url;
1480        a.download = filename;
1481        a.click();
1482        URL.revokeObjectURL(url);
1483
1484        updateStatus(`Exported ${state.extractedURLs.length} URLs as ${format.toUpperCase()}!`, 'success');
1485    }
1486
1487    async function clearURLs() {
1488        if (!confirm('Are you sure you want to clear all extracted URLs? This cannot be undone.')) {
1489            return;
1490        }
1491
1492        await clearState();
1493        updateUI();
1494        updateStatus('All URLs cleared', 'info');
1495    }
1496
1497    // ============================================
1498    // PAGE DETECTION AND AUTO-PROCESSING
1499    // ============================================
1500
1501    function detectPageType() {
1502        const url = window.location.href;
1503        
1504        if (url.includes('/videos') || document.querySelectorAll('a.SceneThumb-SceneImageLink-Link[href*="/video/"]').length > 0) {
1505            return 'listing';
1506        } else if (url.includes('/video/') && document.querySelector('button.ScenePlayerHeaderPlus-WatchEpisodeButton-Button')) {
1507            return 'video';
1508        }
1509        
1510        return 'unknown';
1511    }
1512
1513    // ============================================
1514    // INITIALIZATION
1515    // ============================================
1516
1517    async function init() {
1518        console.log('[Init] Adult Time Video URL Extractor initialized');
1519        
1520        // Setup network interceptor first
1521        setupNetworkInterceptor();
1522        
1523        // Load saved state
1524        await loadState();
1525        
1526        // Detect page type
1527        const pageType = detectPageType();
1528        console.log(`[Init] Page type detected: ${pageType}`);
1529        
1530        // Create UI
1531        createControlPanel();
1532        
1533        // Update UI with loaded state
1534        updateUI();
1535        
1536        // If we're on a video page and extraction is running, process it
1537        if (pageType === 'video' && state.isRunning && !state.isPaused) {
1538            console.log('[Init] On video page with active extraction, processing...');
1539            await sleep(1000);
1540            await processCurrentVideoPage();
1541        }
1542        
1543        console.log('[Init] Extension ready!');
1544    }
1545
1546    // Wait for page to load
1547    if (document.readyState === 'loading') {
1548        document.addEventListener('DOMContentLoaded', init);
1549    } else {
1550        init();
1551    }
1552
1553})();