245 lines
6.4 KiB
Dart
245 lines
6.4 KiB
Dart
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<PreferredLanguageOption> options =
|
|
<PreferredLanguageOption>[
|
|
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<VideoItem> rankVideos(
|
|
List<VideoItem> 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 <String>[
|
|
' 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 <String>[
|
|
'english dub',
|
|
'english dubbed',
|
|
'dubbed in english',
|
|
'english audio',
|
|
],
|
|
'ar' => const <String>[
|
|
'مدبلج عربي',
|
|
'مدبلجة عربية',
|
|
'دبلجة عربية',
|
|
'باللغة العربية',
|
|
],
|
|
'de' => const <String>[
|
|
'deutsche synchronisation',
|
|
'deutsch synchronisiert',
|
|
'deutsche synchro',
|
|
'german dub',
|
|
],
|
|
'es' => const <String>[
|
|
'doblaje espanol',
|
|
'doblaje español',
|
|
'version espanola',
|
|
'versión española',
|
|
'spanish dub',
|
|
'audio espanol',
|
|
'audio español',
|
|
],
|
|
_ => const <String>[],
|
|
};
|
|
|
|
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 <String>{
|
|
'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 <String>{
|
|
'the', 'and', 'with', 'for', 'from', 'this', 'that', 'how', 'why',
|
|
'what', 'new', 'best', 'review', 'explained', 'english',
|
|
},
|
|
'de' => const <String>{
|
|
'der', 'die', 'das', 'und', 'mit', 'für', 'fur', 'von', 'ist', 'wie',
|
|
'warum', 'neu', 'neue', 'deutsch', 'erklart', 'erklärt',
|
|
},
|
|
'es' => const <String>{
|
|
'el', 'la', 'los', 'las', 'un', 'una', 'con', 'para', 'por', 'como',
|
|
'cómo', 'nuevo', 'nueva', 'espanol', 'español', 'explicado',
|
|
},
|
|
_ => const <String>{},
|
|
};
|
|
|
|
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() +
|
|
' ';
|
|
}
|
|
}
|