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})();