diff --git a/lib/features/sender/data/services/link_extraction_service.dart b/lib/features/sender/data/services/link_extraction_service.dart index ed98216..f4bdb86 100644 --- a/lib/features/sender/data/services/link_extraction_service.dart +++ b/lib/features/sender/data/services/link_extraction_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -8,26 +9,505 @@ abstract class ILinkExtractionService { } 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; - - // HeadlessWebView'i yerel değişkene alıyoruz ki paralel çalışmalarda çakışmasın HeadlessInAppWebView? headlessWebView; - // Temizlik ve Bellek Yönetimi void cleanup() { timeoutTimer?.cancel(); headlessWebView?.dispose(); headlessWebView = null; } - // Bulunan URL'yi işleme void handleFoundUrl(String url) { if (!completer.isCompleted) { debugPrint("EXTRACTION: SUCCESS! Found: $url"); @@ -45,7 +525,7 @@ class LinkExtractionService implements ILinkExtractionService { } }); - // 2. UserScript: JS Kodunu Iframe'ler dahil tüm sayfalara DOM yüklenmeden ENJEKTE EDİYORUZ + // UserScript: JS Kodunu Iframe'ler dahil tüm sayfalara DOM yüklenmeden ENJEKTE EDİYORUZ final snifferScript = UserScript( source: ''' (function() { @@ -118,7 +598,7 @@ class LinkExtractionService implements ILinkExtractionService { headlessWebView = HeadlessInAppWebView( initialUrlRequest: URLRequest(url: WebUri(pageUrl)), - initialUserScripts: UnmodifiableListView([snifferScript]), // Scripti buradan veriyoruz + initialUserScripts: UnmodifiableListView([snifferScript]), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, isInspectable: true, @@ -170,6 +650,10 @@ class LinkExtractionService implements ILinkExtractionService { return completer.future; } + // ============================================================ + // Yardımcı Fonksiyonlar + // ============================================================ + bool _isVideoUrl(String url) { final lower = url.toLowerCase(); if (lower.startsWith('data:') || lower.length < 10) return false;