188 lines
6.8 KiB
Dart
188 lines
6.8 KiB
Dart
import 'dart:async';
|
||
import 'dart:collection';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||
|
||
abstract class ILinkExtractionService {
|
||
Future<String?> extractVideoUrl(String pageUrl);
|
||
}
|
||
|
||
class LinkExtractionService implements ILinkExtractionService {
|
||
|
||
@override
|
||
Future<String?> extractVideoUrl(String pageUrl) async {
|
||
// 1. Zaten direkt bir video linkiyse hemen döndür
|
||
if (_isVideoUrl(pageUrl)) return pageUrl;
|
||
|
||
final completer = Completer<String?>();
|
||
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");
|
||
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)),
|
||
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)) {
|
||
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;
|
||
}
|
||
|
||
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');
|
||
}
|
||
} |