import '../source.dart'; import 'constants.dart'; class ComicLandUrlParts { const ComicLandUrlParts({ required this.origin, required this.slug, this.chapterIndex, }); final String origin; final String slug; final String? chapterIndex; } bool isLikelyComicLandSite(String url) { try { final host = Uri.parse(url).host.toLowerCase(); return comicLandHosts.contains(host) || host.contains('comicland'); } catch (_) { return false; } } String seriesSlugFromListingUrl(String pageUrl) { try { final parts = Uri.parse(pageUrl).pathSegments.where((s) => s.isNotEmpty).toList(); final comicIdx = parts.indexOf('comic'); if (comicIdx != -1 && comicIdx + 1 < parts.length) { return parts[comicIdx + 1]; } } catch (_) {} return 'comic'; } ComicLandUrlParts parseComicLandUrl(String pageUrl) { final u = Uri.parse(pageUrl); final parts = u.pathSegments.where((s) => s.isNotEmpty).toList(); final comicIdx = parts.indexOf('comic'); final slug = comicIdx != -1 && comicIdx + 1 < parts.length ? parts[comicIdx + 1] : 'comic'; String? chapterIndex; final chapterIdx = parts.indexOf('chapter'); if (chapterIdx != -1 && chapterIdx + 1 < parts.length) { chapterIndex = parts[chapterIdx + 1]; } return ComicLandUrlParts( origin: '${u.scheme}://${u.host}', slug: slug, chapterIndex: chapterIndex, ); } String chapterUrlFromParts(String siteOrigin, String slug, Object chapterIndex) { final origin = siteOrigin.replaceAll(RegExp(r'/+$'), ''); return '$origin/comic/$slug/chapter/$chapterIndex'; } String listingUrlForSlug(String slug) => '${comicLandOrigin.replaceAll(RegExp(r'/+$'), '')}/comic/$slug'; String chapterKeyFromUrl(String chapterUrl) { try { final segs = Uri.parse(chapterUrl).pathSegments.where((s) => s.isNotEmpty).toList(); return segs.isEmpty ? 'chapter' : segs.last; } catch (_) { return 'chapter'; } } String? chapterNumberFromTitle(String title) { final m = RegExp(r'^Chapter\s+([\d.]+)', caseSensitive: false) .firstMatch(title.trim().replaceAll(RegExp(r'\s+'), ' ')); return m?.group(1); } String sanitizeFsStem(String stem) { final cleaned = stem .replaceAll(RegExp(r'[^a-z0-9._-]+', caseSensitive: false), '_') .replaceAll(RegExp(r'^_+|_+$'), ''); if (cleaned.isEmpty) return 'chapter'; return cleaned.length > 180 ? cleaned.substring(0, 180) : cleaned; } String sanitizeComicDirName(String name) { final collapsed = name.trim().replaceAll(RegExp(r'\s+'), ' '); final cleaned = String.fromCharCodes( collapsed.runes.where((code) { if (code < 32 || code == 127) return false; final ch = String.fromCharCode(code); return !RegExp(r'[<>:"/\\|?*]').hasMatch(ch); }), ).replaceAll(RegExp(r'\.+$'), '').trim(); if (cleaned.isEmpty) return 'comic'; return cleaned.length > 180 ? cleaned.substring(0, 180) : cleaned; } String stemForComicLandChapterCbz(String chapterPageUrl) { try { final segs = Uri.parse(chapterPageUrl).pathSegments.where((s) => s.isNotEmpty).toList(); final comicIdx = segs.indexOf('comic'); if (comicIdx != -1 && segs.length >= comicIdx + 4 && segs[comicIdx + 2] == 'chapter') { return '${segs[comicIdx + 1]}-chapter-${segs[comicIdx + 3]}'; } if (segs.length >= 2) { return '${segs[segs.length - 2]}-${segs.last}'; } return segs.isEmpty ? 'chapter' : segs.last; } catch (_) { return 'chapter'; } } String transferLinkBasename(String? chapterNumber, String? label, String? cbzPath) { final candidates = [chapterNumber, label] .whereType() .where((s) => s.trim().isNotEmpty); for (final raw in candidates) { final text = raw.trim(); final m = RegExp(r'(\d+)((?:\.\d+)*)([a-zA-Z]*)').firstMatch(text); if (m != null) { final padded = m.group(1)!.padLeft(3, '0'); return 'chapter $padded${m.group(2) ?? ''}${m.group(3) ?? ''}.cbz'; } } if (cbzPath != null && cbzPath.isNotEmpty) { final base = cbzPath.split('/').last; if (base.isNotEmpty) return base; } return 'chapter unknown.cbz'; } CatalogComic catalogFromJson(Map row) { final slug = (row['slug'] as String?)?.trim() ?? 'comic'; return CatalogComic( source: SourceId.comicland, slug: slug, title: (row['title'] as String?)?.trim().isNotEmpty == true ? (row['title'] as String).trim() : slug, listingUrl: listingUrlForSlug(slug), coverUrl: row['cover_url'] as String?, description: row['description'] as String?, rating: (row['rating'] is num) ? (row['rating'] as num).toDouble() : null, ); }