feat: add LinkExtractionService with specialized support for hdfilmizle video extraction

This commit is contained in:
Ümit Tunç
2026-04-29 23:33:38 +03:00
parent 654afdad09
commit ff8a39e5a3
@@ -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';
@@ -14,20 +15,499 @@ class LinkExtractionService implements ILinkExtractionService {
// 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<String?> _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<String?> _extractIframeUrl(String pageUrl) async {
final completer = Completer<String?>();
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<UserScript>([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<dynamic> 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<String?> _extractVideoFromIframe(String iframeUrl) async {
final completer = Completer<String?>();
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<UserScript>([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<String?> _extractGeneric(String pageUrl) async {
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");
@@ -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<UserScript>([snifferScript]), // Scripti buradan veriyoruz
initialUserScripts: UnmodifiableListView<UserScript>([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;