@@ -1,3 +1,6 @@
import ' dart:async ' ;
import ' dart:collection ' ;
import ' package:flutter/foundation.dart ' ;
import ' package:flutter_inappwebview/flutter_inappwebview.dart ' ;
abstract class ILinkExtractionService {
@@ -5,39 +8,181 @@ abstract class ILinkExtractionService {
}
class LinkExtractionService implements ILinkExtractionService {
HeadlessInAppWebView ? _headlessWebView ;
@ override
Future < String ? > extractVideoUrl ( String pageUrl ) async {
// 1. Check if it's already a video link
// 1. Zaten direkt bir video linkiyse hemen döndür
if ( _isVideoUrl ( pageUrl ) ) return pageUrl ;
// 2. Use Headless WebView to find the video
String ? foundUrl ;
final completer = Completer < String ? > ( ) ;
Timer ? timeoutTimer ;
_headlessWebView = HeadlessInAppWebView (
// 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 " ) ;
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 ) ;
}
} ) ;
// 2. 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 ) ) ,
onLoadResource: ( controller , resource ) {
final url = resource . url . toString ( ) ;
initialUserScripts: UnmodifiableListView < UserScript > ( [ snifferScript ] ) , // Scripti buradan veriyoruz
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 ) ) {
foundUrl = url ;
_headlessWebView ? . dispose ( ) ;
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 ( ) ;
// Wait for some time to catch resources
await Future . delayed ( const Duration ( seconds: 5 ) ) ;
return foundUrl ;
await headlessWebView ? . run ( ) ;
return completer . future ;
}
bool _isVideoUrl ( String url ) {
final lower = url . toLowerCase ( ) ;
return lower . contains ( ' .m3u8 ' ) | |
lower . contains ( ' .mp4 ' ) | |
lower . contains ( ' .mkv ' ) ;
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 ' ) ;
}
}
}