feat: implement LinkExtractionService using HeadlessInAppWebView to intercept and extract video stream URLs
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
|
|
||||||
@@ -6,57 +8,181 @@ abstract class ILinkExtractionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LinkExtractionService implements ILinkExtractionService {
|
class LinkExtractionService implements ILinkExtractionService {
|
||||||
HeadlessInAppWebView? _headlessWebView;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> extractVideoUrl(String pageUrl) async {
|
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;
|
if (_isVideoUrl(pageUrl)) return pageUrl;
|
||||||
|
|
||||||
// 2. Use Headless WebView to find the video
|
final completer = Completer<String?>();
|
||||||
String? foundUrl;
|
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)),
|
initialUrlRequest: URLRequest(url: WebUri(pageUrl)),
|
||||||
|
initialUserScripts: UnmodifiableListView<UserScript>([snifferScript]), // Scripti buradan veriyoruz
|
||||||
initialSettings: InAppWebViewSettings(
|
initialSettings: InAppWebViewSettings(
|
||||||
javaScriptEnabled: true,
|
javaScriptEnabled: true,
|
||||||
isInspectable: true,
|
isInspectable: true,
|
||||||
|
mediaPlaybackRequiresUserGesture: false, // ÇOK ÖNEMLİ: Videoların tıklanmadan yüklenmesine izin ver
|
||||||
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
|
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
|
||||||
userAgent: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36",
|
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 {
|
onReceivedServerTrustAuthRequest: (controller, challenge) async {
|
||||||
return ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED);
|
return ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED);
|
||||||
},
|
},
|
||||||
onConsoleMessage: (controller, consoleMessage) {
|
shouldInterceptRequest: (controller, request) async {
|
||||||
debugPrint("WEBVIEW CONSOLE: ${consoleMessage.message}");
|
final url = request.url.toString();
|
||||||
},
|
|
||||||
onLoadError: (controller, url, code, message) {
|
|
||||||
debugPrint("WEBVIEW ERROR: $message ($code) for $url");
|
|
||||||
},
|
|
||||||
onLoadHttpError: (controller, url, statusCode, description) {
|
|
||||||
debugPrint("WEBVIEW HTTP ERROR: $description ($statusCode) for $url");
|
|
||||||
},
|
|
||||||
onLoadResource: (controller, resource) {
|
|
||||||
final url = resource.url.toString();
|
|
||||||
if (_isVideoUrl(url)) {
|
if (_isVideoUrl(url)) {
|
||||||
foundUrl = url;
|
handleFoundUrl(url);
|
||||||
_headlessWebView?.dispose();
|
}
|
||||||
|
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();
|
await headlessWebView?.run();
|
||||||
|
return completer.future;
|
||||||
// Wait for some time to catch resources
|
|
||||||
await Future.delayed(const Duration(seconds: 5));
|
|
||||||
|
|
||||||
return foundUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isVideoUrl(String url) {
|
bool _isVideoUrl(String url) {
|
||||||
final lower = url.toLowerCase();
|
final lower = url.toLowerCase();
|
||||||
return lower.contains('.m3u8') ||
|
if (lower.startsWith('data:') || lower.length < 10) return false;
|
||||||
lower.contains('.mp4') ||
|
|
||||||
lower.contains('.mkv');
|
// 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user