import '../models/video_item.dart'; class PreferredLanguageOption { const PreferredLanguageOption({ required this.code, required this.label, }); final String code; final String label; } class PreferredLanguageService { PreferredLanguageService._(); static const List options = [ PreferredLanguageOption(code: 'auto', label: 'Auto'), PreferredLanguageOption(code: 'fr', label: 'Français (fr)'), PreferredLanguageOption(code: 'en', label: 'English (en)'), PreferredLanguageOption(code: 'ar', label: 'العربية (ar)'), PreferredLanguageOption(code: 'de', label: 'Deutsch (de)'), PreferredLanguageOption(code: 'es', label: 'Español (es)'), ]; static bool isSupported(String code) { return options.any((option) => option.code == code); } static String labelFor(String code) { for (final option in options) { if (option.code == code) { return option.label; } } return 'Auto'; } static String nativeSearchHint(String code) { return switch (code) { 'fr' => 'français', 'en' => 'English', 'ar' => 'عربي', 'de' => 'Deutsch', 'es' => 'español', _ => '', }; } static String dubbingSearchHint(String code) { return switch (code) { 'fr' => 'VF doublage français version française', 'en' => 'English dub dubbed', 'ar' => 'مدبلج عربي دبلجة عربية', 'de' => 'deutsche Synchronisation Deutsch', 'es' => 'doblaje español versión española', _ => '', }; } /// Returns a positive local boost. This is deliberately a preference, /// never a hard filter: videos in other languages remain eligible. static int scoreVideo(VideoItem video, String preferredLanguage) { if (preferredLanguage == 'auto') { return 0; } final raw = '${video.title} ${video.description} ${video.author}'; final text = _normalize(raw); var score = 0; if (_hasDubbingMarker(text, preferredLanguage)) { score += 18; score += _popularityBonus(video.viewCount); } if (_looksLikeLanguage(text, preferredLanguage)) { score += 12; } return score.clamp(0, 32).toInt(); } static bool isLikelyDubbed(VideoItem video, String preferredLanguage) { if (preferredLanguage == 'auto') { return false; } final text = _normalize( '${video.title} ${video.description} ${video.author}', ); return _hasDubbingMarker(text, preferredLanguage); } static List rankVideos( List videos, String preferredLanguage, ) { if (preferredLanguage == 'auto' || videos.length < 2) { return videos; } final indexed = videos.asMap().entries.toList(growable: false); final sorted = [...indexed] ..sort((a, b) { final aScore = scoreVideo(a.value, preferredLanguage); final bScore = scoreVideo(b.value, preferredLanguage); final scoreCompare = bScore.compareTo(aScore); if (scoreCompare != 0) { return scoreCompare; } return a.key.compareTo(b.key); }); return sorted.map((entry) => entry.value).toList(growable: false); } static int _popularityBonus(int? viewCount) { if (viewCount == null || viewCount <= 0) { return 0; } if (viewCount >= 10000000) { return 8; } if (viewCount >= 1000000) { return 6; } if (viewCount >= 100000) { return 4; } if (viewCount >= 10000) { return 2; } return 1; } static bool _hasDubbingMarker(String text, String code) { final markers = switch (code) { 'fr' => const [ ' vf ', '[vf]', '(vf)', 'version francaise', 'version française', 'double en francais', 'doublé en français', 'doublage francais', 'doublage français', 'french dub', 'french dubbed', 'audio francais', 'audio français', ], 'en' => const [ 'english dub', 'english dubbed', 'dubbed in english', 'english audio', ], 'ar' => const [ 'مدبلج عربي', 'مدبلجة عربية', 'دبلجة عربية', 'باللغة العربية', ], 'de' => const [ 'deutsche synchronisation', 'deutsch synchronisiert', 'deutsche synchro', 'german dub', ], 'es' => const [ 'doblaje espanol', 'doblaje español', 'version espanola', 'versión española', 'spanish dub', 'audio espanol', 'audio español', ], _ => const [], }; return markers.any(text.contains); } static bool _looksLikeLanguage(String text, String code) { if (code == 'ar') { final arabic = RegExp(r'[\u0600-\u06FF]'); final matches = arabic.allMatches(text).length; return matches >= 5; } final words = text .split(RegExp(r'\s+')) .where((word) => word.length >= 2) .toList(growable: false); final markers = switch (code) { 'fr' => const { 'le', 'la', 'les', 'un', 'une', 'des', 'du', 'de', 'dans', 'avec', 'pour', 'sur', 'est', 'sont', 'comment', 'pourquoi', 'nouveau', 'nouvelle', 'francais', 'français', 'voici', 'tout', 'tous', }, 'en' => const { 'the', 'and', 'with', 'for', 'from', 'this', 'that', 'how', 'why', 'what', 'new', 'best', 'review', 'explained', 'english', }, 'de' => const { 'der', 'die', 'das', 'und', 'mit', 'für', 'fur', 'von', 'ist', 'wie', 'warum', 'neu', 'neue', 'deutsch', 'erklart', 'erklärt', }, 'es' => const { 'el', 'la', 'los', 'las', 'un', 'una', 'con', 'para', 'por', 'como', 'cómo', 'nuevo', 'nueva', 'espanol', 'español', 'explicado', }, _ => const {}, }; var hits = 0; for (final word in words) { if (markers.contains(word)) { hits++; } } return hits >= 2; } static String _normalize(String value) { return ' ${value.toLowerCase()}' .replaceAll(RegExp(r'[^a-z0-9à-ÿ\u0600-\u06ff\[\]\(\)]+'), ' ') .replaceAll(RegExp(r'\s+'), ' ') .trimRight() + ' '; } }