Files
fastwatcher/lib/features/sender/data/services/link_extraction_service.dart
T

188 lines
6.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
}
}