import '../models/cloud_media_link.dart'; class CloudLinkExtractor { static final _urlRegex = RegExp( r'''https?://[^\s<>"{}|\\^`\[\]]+''', caseSensitive: false, ); static final _htmlLinkRegex = RegExp( r"""]*href=["']([^"']+)["'][^>]*>(.*?)""", caseSensitive: false, dotAll: true, ); static final _passwordRegex = RegExp( r'(?:pass(?:word)?|pw|пароль|пас)(?:[\s:=_-]+)([^\s,;<>"\n\r]{2,50})', caseSensitive: false, ); /// Extracts any archive password mentioned in the raw text/HTML. static String? extractPassword(String? text) { if (text == null || text.trim().isEmpty) return null; final match = _passwordRegex.firstMatch(text); if (match != null && match.groupCount >= 1) { final pass = match.group(1)?.trim(); if (pass != null && pass.isNotEmpty && !pass.startsWith('http') && pass.toLowerCase() != 'none') { return pass; } } return null; } /// Extracts all cloud media and video mirror links from text, HTML, and embeds. static List extractLinks({ String? content, String? source, Map? embed, }) { final results = []; final seenUrls = {}; final detectedPass = extractPassword(content); void addLink(String rawUrl, {String? customTitle}) { final cleanUrl = _cleanUrl(rawUrl); if (cleanUrl == null || seenUrls.contains(cleanUrl)) return; seenUrls.add(cleanUrl); final link = _classifyLink(cleanUrl, customTitle: customTitle, password: detectedPass); if (link != null) { results.add(link); } } // 1. Process HTML anchors first to preserve anchor text / custom title if (content != null && content.contains(''))) { url = url.substring(0, url.length - 1); } final parsed = Uri.tryParse(url); if (parsed == null || !parsed.hasScheme || (!parsed.scheme.startsWith('http'))) { return null; } return url; } static String _stripHtml(String? html) { if (html == null) return ''; return html.replaceAll(RegExp(r'<[^>]*>'), '').trim(); } static CloudMediaLink? _classifyLink( String url, { String? customTitle, String? password, }) { final uri = Uri.tryParse(url); if (uri == null) return null; final host = uri.host.toLowerCase(); final path = uri.path.toLowerCase(); // Google Drive if (host.contains('drive.google.com')) { final isFolder = path.contains('/folders/') || uri.queryParameters.containsKey('folder'); String? directStreamUrl; final fileIdMatch = RegExp(r'/file/d/([a-zA-Z0-9_-]+)').firstMatch(uri.path); final idParam = fileIdMatch?.group(1) ?? uri.queryParameters['id']; if (idParam != null && !isFolder) { directStreamUrl = 'https://drive.google.com/uc?export=download&id=$idParam'; } return CloudMediaLink( url: url, service: CloudServiceType.googleDrive, title: customTitle ?? (isFolder ? 'Google Drive Folder' : 'Google Drive File'), directStreamUrl: directStreamUrl, isFolder: isFolder, isStreamable: directStreamUrl != null, detectedPassword: password, ); } // MEGA if (host.contains('mega.nz') || host.contains('mega.co.nz')) { final isFolder = url.contains('/folder/') || url.contains('/#F!') || url.contains('#F!'); return CloudMediaLink( url: url, service: CloudServiceType.mega, title: customTitle ?? (isFolder ? 'MEGA Cloud Folder' : 'MEGA Video / Archive'), directStreamUrl: null, // End-to-end encrypted; launches in MEGA app/browser isFolder: isFolder, isStreamable: false, detectedPassword: password, ); } // Dropbox if (host.contains('dropbox.com')) { final isFolder = path.contains('/sh/') || path.contains('/folder'); var direct = url; if (direct.contains('?dl=0')) { direct = direct.replaceAll('?dl=0', '?raw=1'); } else if (!direct.contains('?raw=1') && !isFolder) { direct = direct.contains('?') ? '$direct&raw=1' : '$direct?raw=1'; } return CloudMediaLink( url: url, service: CloudServiceType.dropbox, title: customTitle ?? (isFolder ? 'Dropbox Folder' : 'Dropbox Media'), directStreamUrl: isFolder ? null : direct, isFolder: isFolder, isStreamable: !isFolder, detectedPassword: password, ); } // Pixeldrain if (host.contains('pixeldrain.com')) { final fileMatch = RegExp(r'/u/([a-zA-Z0-9_-]+)').firstMatch(uri.path); final isList = path.contains('/l/'); final fileId = fileMatch?.group(1); final direct = fileId != null ? 'https://pixeldrain.com/api/file/$fileId' : null; return CloudMediaLink( url: url, service: CloudServiceType.pixeldrain, title: customTitle ?? (isList ? 'Pixeldrain Gallery/List' : 'Pixeldrain Video'), directStreamUrl: direct, isFolder: isList, isStreamable: direct != null, detectedPassword: password, ); } // Catbox / Litterbox if (host.contains('catbox.moe')) { final isVideo = path.endsWith('.mp4') || path.endsWith('.webm') || path.endsWith('.mov') || path.endsWith('.m4v'); return CloudMediaLink( url: url, service: CloudServiceType.catbox, title: customTitle ?? 'Catbox Upload', directStreamUrl: url, isFolder: false, isStreamable: isVideo, detectedPassword: password, ); } // MediaFire if (host.contains('mediafire.com')) { final isFolder = path.contains('/folder/') || path.contains('/?'); return CloudMediaLink( url: url, service: CloudServiceType.mediafire, title: customTitle ?? (isFolder ? 'MediaFire Folder' : 'MediaFire Download'), directStreamUrl: null, isFolder: isFolder, isStreamable: false, detectedPassword: password, ); } // Bunkr if (host.contains('bunkr.')) { final isAlbum = path.contains('/a/') || path.contains('/v/'); return CloudMediaLink( url: url, service: CloudServiceType.bunkr, title: customTitle ?? (isAlbum ? 'Bunkr Album' : 'Bunkr Media'), directStreamUrl: null, isFolder: isAlbum, isStreamable: false, detectedPassword: password, ); } // GoFile if (host.contains('gofile.io')) { return CloudMediaLink( url: url, service: CloudServiceType.gofile, title: customTitle ?? 'GoFile Folder / Archive', directStreamUrl: null, isFolder: true, isStreamable: false, detectedPassword: password, ); } // TeraBox if (host.contains('terabox') || host.contains('1024tera') || host.contains('dubox')) { return CloudMediaLink( url: url, service: CloudServiceType.terabox, title: customTitle ?? 'TeraBox Cloud Share', directStreamUrl: null, isFolder: true, isStreamable: false, detectedPassword: password, ); } // Direct Video Link (MP4, WEBM, MKV, MOV) if (path.endsWith('.mp4') || path.endsWith('.webm') || path.endsWith('.mov') || path.endsWith('.m4v') || path.endsWith('.mkv')) { final filename = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : 'Video Stream'; return CloudMediaLink( url: url, service: CloudServiceType.genericVideo, title: customTitle ?? filename, directStreamUrl: url, isFolder: false, isStreamable: true, detectedPassword: password, ); } // Ignore known booru and search engine domains if (host.contains('kemono') || host.contains('coomer') || host.contains('danbooru') || host.contains('gelbooru') || host.contains('google.com') && !host.contains('drive') || host.contains('twitter.com') || host.contains('x.com') || host.contains('patreon.com') || host.contains('fanbox.cc') || host.contains('pixiv.net')) { return null; } return null; } /// Extracts clean, readable text (stripping HTML tags and formatting links) suitable for author commentary and announcements. static String cleanCommentary(String? rawHtml) { if (rawHtml == null || rawHtml.trim().isEmpty) return ''; var text = rawHtml; // Format anchor tags: TEXT text = text.replaceAllMapped( RegExp(r"""]*href=["']([^"']+)["'][^>]*>(.*?)""", caseSensitive: false, dotAll: true), (match) { final href = match.group(1)?.trim() ?? ''; final label = match.group(2)?.replaceAll(RegExp(r'<[^>]*>'), '').trim() ?? ''; if (label.isEmpty || label == href || href.contains(label)) { return href; } return '$label ($href)'; }, ); // Convert line breaks and paragraph endings text = text .replaceAll(RegExp(r'', caseSensitive: false), '\n') .replaceAll(RegExp(r'<\/p\s*>', caseSensitive: false), '\n\n') .replaceAll(RegExp(r'<\/div\s*>', caseSensitive: false), '\n') .replaceAll(RegExp(r'', caseSensitive: false), '• ') .replaceAll(RegExp(r'<\/li\s*>', caseSensitive: false), '\n'); // Strip remaining HTML tags text = text.replaceAll(RegExp(r'<[^>]*>'), ''); // Decode HTML entities text = text .replaceAll(' ', ' ') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll(''', "'") .replaceAll(''', "'"); // Normalize multiple consecutive blank lines text = text.replaceAll(RegExp(r'\n{3,}'), '\n\n'); return text.trim(); } }