Files

672 lines
24 KiB
Dart
Raw Permalink 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 'dart:convert';
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;
// 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;
HeadlessInAppWebView? headlessWebView;
void cleanup() {
timeoutTimer?.cancel();
headlessWebView?.dispose();
headlessWebView = null;
}
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);
}
});
// 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]),
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;
}
// ============================================================
// Yardımcı Fonksiyonlar
// ============================================================
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');
}
}