import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; abstract class ILinkExtractionService { Future extractVideoUrl(String pageUrl); } class LinkExtractionService implements ILinkExtractionService { @override Future extractVideoUrl(String pageUrl) async { // 1. Zaten direkt bir video linkiyse hemen döndür if (_isVideoUrl(pageUrl)) return pageUrl; // 2. hdfilmizle.so gibi bilinen siteleri özel olarak işle if (_isHdFilmIzle(pageUrl)) { debugPrint("EXTRACTION: Detected hdfilmizle.so — using specialized extractor"); return _extractFromHdFilmIzle(pageUrl); } // 3. Genel amaçlı sniffer (diğer siteler için) return _extractGeneric(pageUrl); } // ============================================================ // hdfilmizle.so Özel Extractor // ============================================================ bool _isHdFilmIzle(String url) { final lower = url.toLowerCase(); return lower.contains('hdfilmizle.so') || lower.contains('hdfilmizle.co') || lower.contains('hdfilmizle.org') || lower.contains('hdfilmizle.net'); } /// hdfilmizle.so için 2 aşamalı extraction: /// Aşama 1: Ana sayfadan "parts" array'ini çıkar → iframe URL al /// Aşama 2: iframe (vidrame.pro) sayfasından EE.dd() ile m3u8 URL decode et Future _extractFromHdFilmIzle(String pageUrl) async { // AŞAMA 1: Ana sayfadan iframe URL'lerini çıkar debugPrint("EXTRACTION [PHASE 1]: Loading main page to extract iframe URLs..."); final iframeUrl = await _extractIframeUrl(pageUrl); if (iframeUrl == null) { debugPrint("EXTRACTION [PHASE 1]: No iframe URL found, falling back to generic extractor"); return _extractGeneric(pageUrl); } debugPrint("EXTRACTION [PHASE 1]: Found iframe URL: $iframeUrl"); // AŞAMA 2: iframe sayfasından video URL'yi decode et debugPrint("EXTRACTION [PHASE 2]: Loading iframe to decode video URL..."); final videoUrl = await _extractVideoFromIframe(iframeUrl); if (videoUrl != null) { debugPrint("EXTRACTION [PHASE 2]: SUCCESS! Video URL: $videoUrl"); return videoUrl; } debugPrint("EXTRACTION [PHASE 2]: Failed, falling back to generic extractor"); return _extractGeneric(pageUrl); } /// Aşama 1: hdfilmizle.so ana sayfasından "parts" JavaScript değişkenini parse eder /// ve ilk iframe URL'sini döndürür. Future _extractIframeUrl(String pageUrl) async { final completer = Completer(); HeadlessInAppWebView? webView; Timer? timeout; void cleanup() { timeout?.cancel(); webView?.dispose(); webView = null; } timeout = Timer(const Duration(seconds: 12), () { if (!completer.isCompleted) { debugPrint("EXTRACTION [PHASE 1]: Timeout"); cleanup(); completer.complete(null); } }); // JS: "parts" array'ini bul ve iframe src'lerini çıkar final extractScript = UserScript( source: ''' (function() { if (window.__phase1Injected) return; window.__phase1Injected = true; // parts array'inden iframe URL çıkar window.__extractIframeUrls = function() { try { // 1. Sayfadaki tüm script taglarını tara var scripts = document.querySelectorAll('script'); for (var i = 0; i < scripts.length; i++) { var text = scripts[i].textContent || scripts[i].innerText || ''; // "let parts" veya "var parts" değişkenini bul var partsMatch = text.match(/(?:let|var|const)\\s+parts\\s*=\\s*(\\[.*?\\]);/s); if (partsMatch) { try { var partsData = eval('(' + partsMatch[1] + ')'); var iframeUrls = []; for (var j = 0; j < partsData.length; j++) { var part = partsData[j]; var data = part.data || part.html || ''; // iframe src'yi çıkar var iframeMatch = data.match(/src=["']([^"']+)["']/i); if (iframeMatch) { iframeUrls.push({ title: part.title || part.name || ('Part ' + j), url: iframeMatch[1] }); } } if (iframeUrls.length > 0) { window.flutter_inappwebview.callHandler('iframeUrlsFound', JSON.stringify(iframeUrls)); return; } } catch(e) { console.log('PHASE1: Error parsing parts: ' + e); } } } // 2. Fallback: Sayfadaki mevcut iframe'leri doğrudan kontrol et var iframes = document.querySelectorAll('iframe'); var directUrls = []; for (var k = 0; k < iframes.length; k++) { var src = iframes[k].src || iframes[k].getAttribute('data-src') || ''; if (src && src.indexOf('vidrame') > -1 || src.indexOf('vidmoly') > -1 || src.indexOf('closeload') > -1 || src.indexOf('rapidezvous') > -1 || src.indexOf('player') > -1) { directUrls.push({ title: 'iframe-' + k, url: src }); } } if (directUrls.length > 0) { window.flutter_inappwebview.callHandler('iframeUrlsFound', JSON.stringify(directUrls)); return; } // 3. Fallback: data-url veya benzeri attribute'lerden var playerElements = document.querySelectorAll('[data-url], [data-src], [data-iframe]'); var attrUrls = []; for (var m = 0; m < playerElements.length; m++) { var u = playerElements[m].getAttribute('data-url') || playerElements[m].getAttribute('data-src') || playerElements[m].getAttribute('data-iframe') || ''; if (u && u.startsWith('http')) { attrUrls.push({ title: 'data-' + m, url: u }); } } if (attrUrls.length > 0) { window.flutter_inappwebview.callHandler('iframeUrlsFound', JSON.stringify(attrUrls)); } } catch(e) { console.log('PHASE1: Top-level error: ' + e); } }; })(); ''', injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, forMainFrameOnly: true, ); webView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(pageUrl)), initialUserScripts: UnmodifiableListView([extractScript]), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, mediaPlaybackRequiresUserGesture: true, mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, supportMultipleWindows: false, javaScriptCanOpenWindowsAutomatically: false, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", ), onWebViewCreated: (controller) { controller.addJavaScriptHandler( handlerName: 'iframeUrlsFound', callback: (args) { if (args.isNotEmpty && !completer.isCompleted) { try { final List urls = jsonDecode(args[0].toString()); if (urls.isNotEmpty) { final firstUrl = urls[0]['url']?.toString(); debugPrint("EXTRACTION [PHASE 1]: Found ${urls.length} iframe(s), first: $firstUrl"); cleanup(); completer.complete(firstUrl); } } catch (e) { debugPrint("EXTRACTION [PHASE 1]: JSON parse error: $e"); } } }, ); }, onReceivedServerTrustAuthRequest: (controller, challenge) async { return ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED); }, onLoadStop: (controller, url) async { debugPrint("EXTRACTION [PHASE 1]: Page loaded: $url"); // Sayfa yüklendikten sonra extraction fonksiyonunu çalıştır for (int i = 0; i < 5; i++) { if (completer.isCompleted) break; await Future.delayed(const Duration(milliseconds: 800)); if (completer.isCompleted) break; try { await controller.evaluateJavascript( source: "if(window.__extractIframeUrls) window.__extractIframeUrls();", ); } catch (e) { debugPrint("EXTRACTION [PHASE 1]: JS execution error: $e"); } } }, ); await webView?.run(); return completer.future; } /// Aşama 2: vidrame.pro iframe sayfasından video URL'yi decode eder. /// Sayfa JWPlayer kullanır ve video kaynağı EE.dd() fonksiyonu ile şifrelenir. /// Strateji: Sayfanın kendi JS'ini çalıştırarak decode edilmiş URL'yi alıyoruz. Future _extractVideoFromIframe(String iframeUrl) async { final completer = Completer(); HeadlessInAppWebView? webView; Timer? timeout; void cleanup() { timeout?.cancel(); webView?.dispose(); webView = null; } void handleFoundUrl(String url) { if (!completer.isCompleted) { cleanup(); completer.complete(url); } } timeout = Timer(const Duration(seconds: 15), () { if (!completer.isCompleted) { debugPrint("EXTRACTION [PHASE 2]: Timeout"); cleanup(); completer.complete(null); } }); // JS: vidrame.pro sayfasından video URL'yi çıkar // EE.dd() fonksiyonu sayfada zaten tanımlı, biz sadece configs.sources[0].file değerini alıyoruz final decodeScript = UserScript( source: ''' (function() { if (window.__phase2Injected) return; window.__phase2Injected = true; console.log("PHASE2: Injected into: " + window.location.href); // Yöntem 1: Network request'leri yakala (en güvenilir) function reportUrl(url) { if (url && (url.includes('.m3u8') || url.includes('.mp4'))) { console.log("PHASE2: Found video URL via intercept: " + url); window.flutter_inappwebview.callHandler('videoUrlFound', url); } } var oldOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { reportUrl(url); return oldOpen.apply(this, arguments); }; var oldFetch = window.fetch; window.fetch = function() { var url = (arguments[0] instanceof Request) ? arguments[0].url : arguments[0]; if (url) reportUrl(url.toString()); return oldFetch.apply(this, arguments); }; // Yöntem 2: JWPlayer configs'ten doğrudan çıkar window.__extractFromJwPlayer = function() { try { // jwplayer instance'ını kontrol et if (typeof jwplayer !== 'undefined') { var pl = jwplayer(); if (pl && pl.getPlaylistItem) { var item = pl.getPlaylistItem(); if (item && item.file) { reportUrl(item.file); return true; } if (item && item.sources) { for (var i = 0; i < item.sources.length; i++) { if (item.sources[i].file) { reportUrl(item.sources[i].file); return true; } } } } } } catch(e) { console.log("PHASE2: JWPlayer check error: " + e); } try { // configs variable'ını doğrudan kontrol et if (typeof configs !== 'undefined' && configs.sources) { for (var j = 0; j < configs.sources.length; j++) { if (configs.sources[j].file) { reportUrl(configs.sources[j].file); return true; } } } } catch(e) { console.log("PHASE2: configs check error: " + e); } try { // EE.dd decode fonksiyonunu doğrudan dene if (typeof EE !== 'undefined' && EE.dd) { // Sayfadaki tüm scriptleri tara ve EE.dd(...) çağrılarını bul var scripts = document.querySelectorAll('script'); for (var k = 0; k < scripts.length; k++) { var text = scripts[k].textContent || ''; var matches = text.match(/EE\\.dd\\(["']([^"']+)["']\\)/g); if (matches) { for (var m = 0; m < matches.length; m++) { var encoded = matches[m].match(/EE\\.dd\\(["']([^"']+)["']\\)/); if (encoded && encoded[1]) { try { var decoded = EE.dd(encoded[1]); if (decoded) reportUrl(decoded); } catch(e2) {} } } } } } } catch(e) { console.log("PHASE2: EE.dd scan error: " + e); } try { // HTML içinde doğrudan m3u8/mp4 regex tara var html = document.documentElement.innerHTML; var match = html.match(/https?:\\/\\/[^"'\\s<>]+\\.(m3u8|mp4)[^"'\\s<>]*/i); if (match) { reportUrl(match[0]); return true; } } catch(e) {} try { // Video elementlerini kontrol et document.querySelectorAll('video').forEach(function(v) { if (v.src && v.src.length > 5) reportUrl(v.src); v.querySelectorAll('source').forEach(function(s) { if (s.src) reportUrl(s.src); }); }); } catch(e) {} return false; }; })(); ''', injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, forMainFrameOnly: false, ); webView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(iframeUrl)), initialUserScripts: UnmodifiableListView([decodeScript]), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, mediaPlaybackRequiresUserGesture: false, mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, supportMultipleWindows: false, javaScriptCanOpenWindowsAutomatically: false, useShouldInterceptRequest: true, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", ), onWebViewCreated: (controller) { controller.addJavaScriptHandler( handlerName: 'videoUrlFound', callback: (args) { if (args.isNotEmpty) { handleFoundUrl(args[0].toString()); } }, ); }, onReceivedServerTrustAuthRequest: (controller, challenge) async { return ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED); }, shouldInterceptRequest: (controller, request) async { final url = request.url.toString(); if (_isVideoUrl(url)) { handleFoundUrl(url); } return null; }, onLoadStop: (controller, url) async { debugPrint("EXTRACTION [PHASE 2]: iframe loaded: $url"); // JWPlayer'ın yüklenmesini bekle ve video URL'yi çıkar for (int i = 0; i < 10; i++) { if (completer.isCompleted) break; await Future.delayed(const Duration(seconds: 1)); if (completer.isCompleted) break; try { await controller.evaluateJavascript( source: "if(window.__extractFromJwPlayer) window.__extractFromJwPlayer();", ); } catch (e) { debugPrint("EXTRACTION [PHASE 2]: JS execution error: $e"); } } }, ); await webView?.run(); return completer.future; } // ============================================================ // Dart tarafında EE.dd() decode (fallback olarak) // ============================================================ /// vidrame.pro'nun EE.dd() şifre çözme algoritması: /// 1. URL-safe base64 → standart base64 dönüştür /// 2. Base64 decode /// 3. ROT13 uygula /// 4. String reverse static String? decodEEdd(String encoded) { try { // URL-safe base64 → standart base64 String s = encoded.replaceAll('-', '+').replaceAll('_', '/'); while (s.length % 4 != 0) { s += '='; } // Base64 decode final decoded = utf8.decode(base64Decode(s)); // ROT13 final rot13 = _rot13(decoded); // String reverse return rot13.split('').reversed.join(''); } catch (e) { debugPrint("decodEEdd error: $e"); return null; } } /// ROT13 dönüşümü (harfler ve rakamlar dahil, vidrame'nin özel versiyonu) static String _rot13(String input) { final buffer = StringBuffer(); for (int i = 0; i < input.length; i++) { final c = input.codeUnitAt(i); if (c >= 65 && c <= 90) { // A-Z buffer.writeCharCode(((c - 65 + 13) % 26) + 65); } else if (c >= 97 && c <= 122) { // a-z buffer.writeCharCode(((c - 97 + 13) % 26) + 97); } else if (c >= 48 && c <= 57) { // 0-9 buffer.writeCharCode(((c - 48 + 13) % 10) + 48); } else { buffer.writeCharCode(c); } } return buffer.toString(); } // ============================================================ // Genel Amaçlı Sniffer (Fallback) // ============================================================ Future _extractGeneric(String pageUrl) async { final completer = Completer(); Timer? timeoutTimer; HeadlessInAppWebView? headlessWebView; void cleanup() { timeoutTimer?.cancel(); headlessWebView?.dispose(); headlessWebView = null; } void handleFoundUrl(String url) { if (!completer.isCompleted) { debugPrint("EXTRACTION: SUCCESS! Found: $url"); cleanup(); completer.complete(url); } } // Zaman aşımı (15 saniye) timeoutTimer = Timer(const Duration(seconds: 15), () { if (!completer.isCompleted) { debugPrint("EXTRACTION: Timeout - no video URL found"); cleanup(); completer.complete(null); } }); // UserScript: JS Kodunu Iframe'ler dahil tüm sayfalara DOM yüklenmeden ENJEKTE EDİYORUZ final snifferScript = UserScript( source: ''' (function() { if (window.snifferInjected) return; window.snifferInjected = true; console.log("SNIFFER: Injected into frame: " + window.location.href); function reportUrl(url) { if (url && (url.includes('.m3u8') || url.includes('.mp4'))) { window.flutter_inappwebview.callHandler('foundVideoUrl', url); } } // 1. Override XMLHttpRequest var oldOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { reportUrl(url); return oldOpen.apply(this, arguments); }; // 2. Override Fetch var oldFetch = window.fetch; window.fetch = function() { var url = (arguments[0] instanceof Request) ? arguments[0].url : arguments[0]; if (url) reportUrl(url.toString()); return oldFetch.apply(this, arguments); }; // 3. Oynatma Butonlarına Tıklama (Tüm iframe'lerde çalışacak) window.clickPlayButtons = function() { var selectors = [ '.play-that-video', '.play-button', '.vjs-big-play-button', '[aria-label="Play"]', '.ytp-large-play-button', '.play-icon', '#play', '.player-play', '.jw-display-icon-container', 'div[class*="play"]', 'button[class*="play"]', 'svg[class*="play"]' ]; selectors.forEach(function(sel) { try { document.querySelectorAll(sel).forEach(function(el) { if (el.offsetWidth > 0 || el.offsetHeight > 0) { el.click(); } }); } catch(e) {} }); // Video etiketlerini direkt kontrol et document.querySelectorAll('video').forEach(function(v) { if (v.src && v.src.length > 5) reportUrl(v.src); v.querySelectorAll('source').forEach(function(s) { if (s.src) reportUrl(s.src); }); }); }; // HTML içindeki linkleri Regex ile tara var scanInterval = setInterval(function() { var html = document.documentElement.innerHTML; var match = html.match(/https?:\\/\\/[^"'\\s<>]+\\.(m3u8|mp4)[^"'\\s<>]*/i); if (match) reportUrl(match[0]); }, 1500); })(); ''', injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, // Sayfa açılmadan hemen önce forMainFrameOnly: false, // ÇOK ÖNEMLİ: Iframe'lerin içine girmesi için false olmalı! ); headlessWebView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(pageUrl)), initialUserScripts: UnmodifiableListView([snifferScript]), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, isInspectable: true, mediaPlaybackRequiresUserGesture: false, // ÇOK ÖNEMLİ: Videoların tıklanmadan yüklenmesine izin ver mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, supportMultipleWindows: false, javaScriptCanOpenWindowsAutomatically: false, useShouldInterceptRequest: true, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", ), onWebViewCreated: (controller) { // JS'den gelen sinyali dinle controller.addJavaScriptHandler(handlerName: 'foundVideoUrl', callback: (args) { if (args.isNotEmpty) { handleFoundUrl(args[0].toString()); } }); }, onReceivedServerTrustAuthRequest: (controller, challenge) async { return ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED); }, shouldInterceptRequest: (controller, request) async { final url = request.url.toString(); if (_isVideoUrl(url)) { handleFoundUrl(url); } return null; }, onLoadStop: (controller, url) async { debugPrint("EXTRACTION: Page loaded: $url"); // JS yüklenmiş mi diye kontrol et ve butonlara tıkla for (int i = 0; i < 8; i++) { if (completer.isCompleted) break; await Future.delayed(const Duration(seconds: 1)); if (completer.isCompleted) break; try { // WebView kapanmışsa hata fırlatmasını engellemek için try-catch await controller.evaluateJavascript(source: "if(window.clickPlayButtons) window.clickPlayButtons();"); } catch (e) { debugPrint("WebView disposed before javascript execution."); } } }, ); await headlessWebView?.run(); return completer.future; } // ============================================================ // Yardımcı Fonksiyonlar // ============================================================ bool _isVideoUrl(String url) { final lower = url.toLowerCase(); if (lower.startsWith('data:') || lower.length < 10) return false; // Analitik veya reklam linklerini es geç if (lower.contains('google.com') || lower.contains('gstatic.com') || lower.contains('analytics')) return false; return lower.contains('.m3u8') || lower.contains('.mp4') || lower.contains('.mkv') || lower.contains('/hls/') || lower.contains('master.m3u8') || lower.contains('index.m3u8') || lower.contains('playlist.m3u8'); } }