341 lines
11 KiB
Dart
341 lines
11 KiB
Dart
import '../models/cloud_media_link.dart';
|
|
|
|
class CloudLinkExtractor {
|
|
static final _urlRegex = RegExp(
|
|
r'''https?://[^\s<>"{}|\\^`\[\]]+''',
|
|
caseSensitive: false,
|
|
);
|
|
|
|
static final _htmlLinkRegex = RegExp(
|
|
r"""<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)</a>""",
|
|
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<CloudMediaLink> extractLinks({
|
|
String? content,
|
|
String? source,
|
|
Map<String, dynamic>? embed,
|
|
}) {
|
|
final results = <CloudMediaLink>[];
|
|
final seenUrls = <String>{};
|
|
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('<a ')) {
|
|
for (final match in _htmlLinkRegex.allMatches(content)) {
|
|
final href = match.group(1);
|
|
final rawTitle = match.group(2);
|
|
final cleanTitle = _stripHtml(rawTitle);
|
|
if (href != null && href.isNotEmpty) {
|
|
addLink(href, customTitle: cleanTitle.isNotEmpty ? cleanTitle : null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Process all URLs in content text
|
|
if (content != null) {
|
|
for (final match in _urlRegex.allMatches(content)) {
|
|
final url = match.group(0);
|
|
if (url != null) addLink(url);
|
|
}
|
|
}
|
|
|
|
// 3. Process source
|
|
if (source != null && source.trim().isNotEmpty) {
|
|
for (final match in _urlRegex.allMatches(source)) {
|
|
final url = match.group(0);
|
|
if (url != null) addLink(url);
|
|
}
|
|
}
|
|
|
|
// 4. Process embed map
|
|
if (embed != null) {
|
|
final embedUrl = embed['url']?.toString();
|
|
if (embedUrl != null && embedUrl.isNotEmpty) {
|
|
final title = embed['title']?.toString();
|
|
addLink(embedUrl, customTitle: title);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
static String? _cleanUrl(String raw) {
|
|
var url = raw.trim();
|
|
// Trim trailing punctuation like ), ., ,, ;, etc.
|
|
while (url.isNotEmpty && (url.endsWith(')') || url.endsWith('.') || url.endsWith(',') || url.endsWith(';') || url.endsWith('>'))) {
|
|
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: <a href="URL">TEXT</a>
|
|
text = text.replaceAllMapped(
|
|
RegExp(r"""<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)</a>""",
|
|
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'<br\s*\/?>', caseSensitive: false), '\n')
|
|
.replaceAll(RegExp(r'<\/p\s*>', caseSensitive: false), '\n\n')
|
|
.replaceAll(RegExp(r'<\/div\s*>', caseSensitive: false), '\n')
|
|
.replaceAll(RegExp(r'<li\s*>', 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();
|
|
}
|
|
}
|