/
web-master
/
IPTV-Aggregator
Обзор
Документация
Войти
/
web-master
/
IPTV-Aggregator
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/classifier.rs
923 строки
45 KB
web-master
Изменение классификатора и добавление возможности работы с локальными файлами плейлистов
04 апр 2026, 19:18
04 апр 2026, 19:18
91d9f73
Код
Авторство
О чём код?
//! Классификатор IPTV каналов по категориям //! //! Этот модуль содержит логику классификации каналов на основе //! ключевых слов в названиях и group-title. //! //! ## Принципы классификации //! //! 1. **Приоритет специфичности** — более специфичные категории проверяются первыми //! 2. **Без конфликтов** — каждое ключевое слово принадлежит только одной категории //! 3. **Без мусора** — только релевантные ключевые слова //! 4. **Реальные каналы** — добавлены точные названия известных IPTV каналов //! 5. **Минимальная длина** — ключевые слова короче 4 символов требуют точного совпадения /// Менеджер категорий каналов /// /// Категории проверяются в порядке специфичности: от наиболее узких к общим. pub struct CategoryClassifier { categories: Vec<(&'static str, Vec<&'static str>)>, } impl CategoryClassifier { /// Создает новый классификатор с предзаполненными категориями pub fn new() -> Self { // Порядок важен! Алгоритм останавливается на первом совпадении. let categories: Vec<(&'static str, Vec<&'static str>)> = vec![ // 1. Армянские — самая специфичная категория ( "Армянские", vec![ "Շանթ", "Shant TV", "Shant TV HD", "Shant", "Shoghakat", "Luys TV", "Luis TV", "Դավ ջան", "Արմենիուս", "Armenius", "Kentron TV", "Kentron", "PanArmenian", "AABC TV", "USArmenia", "Yerkir Media", "Yerkir", "Dar 21", "Nor Hayastan", "Հայկական", "Hayastan TV", "Hayastan", "Azatutyun", "Xaxalik", "LiveNews.am", "ArmNews", "ARTN TV", "Armenia TV", "Armenia 1", "Armenia 2", "армянск", "armenian", "армян", "armen", "hayk", "hayer", "hayastan", "haykakan", "ATV Armenia", "Ararat TV", ], ), // 2. Детские ( "Детские", vec![ "nickelodeon", "nick jr", "nicktoons", "cartoon network", "boomerang", "baby tv", "ti ji", "gulli", "disney channel", "disney junior", "disney xd", "карусель", "детск", "detsk", "для детей", "для малышей", "kids", "children", "junior", "baby", "малыш", "мульт", "мультик", "cartoon", "анимац", "фея", "принцесс", "единорог", "гном", "тролль", "винни пух", "маугл", "тарзан", "мишутк", "смешарик", "фиксик", "лунтик", "барбоск", "маша и медвед", "три кота", "щенок", "котенок", "хомяк", "попугай", ], ), // 3. Спортивные ( "Спортивные", vec![ "матч тв", "матч!", "match tv", "match!", "матч премьер", "матч страна", "матч боец", "матч арена", "eurosport", "евроспорт", "fox sports", "espn", "sky sports", "bt sport", "кхл", "континентальн", "nhl", "nba", "ufc", "fifa", "uefa", "спорт", "sport", "спортивн", "athletic", "атлет", "football", "футбол", "hockey", "хоккей", "бокс", "boxing", "fight", "fighting", "единоборств", "теннис", "tennis", "баскетбол", "basketball", "волейбол", "volleyball", "гандбол", "handball", "биатлон", "biathlon", "лыж", "ski", "фигурн", "коньк", "автогонк", "racing", "формула", "formula", "рыбалк", "охот", "варзиш", "extreme", "экстрим", ], ), // 4. Музыкальные ( "Музыкальные", vec![ "муз-тв", "muz-tv", "муз тв", "муз1", "mtv", "mtv hits", "mtv rocks", "mtv live", "vh1", "trace", "trace urban", "bridge tv", "бридж тв", "музык", "music", "музканал", "музыкальн", "музсоюз", "хит-парад", "хит парад", "hit parade", "чарт", "chart", "шансон", "chanson", "бард", "романс", "поп-музык", "pop music", "рок-музык", "rock music", "джаз", "jazz", "блюз", "blues", "электронн", "electronic", "techno", "house", "trance", "хип-хоп", "hip-hop", "hiphop", "рэп", "rap", "метал", "metal", "панк", "punk", "гранж", "grunge", "латино", "latino", "salsa", "танго", "tango", "регги", "reggae", "кантри", "country", "опер", "opera", "симфонич", "филармон", "клип", "clip", "видеоклип", "концерт", "concert", "live music", "живой звук", "диджей", "дискотек", "k-pop", "kpop", ], ), // 5. Новости ( "Новости", vec![ "euronews", "евроньюс", "rbc", "рбк", "рбк тв", "царьград", "дождь", "tvrain", "новости", "новостн", "вести", "время", "сегодня", "репортаж", "журналист", "корреспондент", "информ", "инфо", "информационн", "события", "известия", "обзор", "репортер", "360 новостей", "новости 24", "россия 24", "russia 24", // Каналы с "24" в названии (новостные) "24 канал", "24 канал", "москва 24", "санкт-петербург", "соловьев", "толк", "т24", ], ), // 6. Религиозные ( "Религиозные", vec![ "православ", "orthodox", "католич", "catholic", "протестант", "protestant", "баптист", "пятидесятник", "адвентист", "мормон", "иудей", "jewish", "раввин", "синагог", "мусульман", "muslim", "ислам", "мечеть", "имам", "будд", "buddh", "индуист", "ведическ", "кришн", "церковь", "church", "храм", "монастырь", "библия", "bible", "евангел", "молитв", "prayer", "проповед", "миссионер", "апостол", "пророк", "богослов", "священ", "spiritual", "духовн", "религиозн", "religion", "союз тв", "троицкий", "радонеж", ], ), // 7. Кинозалы ( "Кинозалы", vec![ "кинозал", "cinema hall", "киноклуб", "kinohall", "kinoclub", "кинохит", "киносемья", ], ), // 8. Кино и сериалы ( "Кино и сериалы", vec![ "кинопоказ", "кинопремьера", "киносерия", "амедиа", "amedia", "start", "okko", "wink", "ivi", " Paramount", "paramount network", "fox movies", "fx", "amc", "кино", "movie", "film", "кинематограф", "cinema", "сериал", "serial", "телесериал", "многосерийн", "блокбастер", "боевик", "экшен", "ужас", "horror", "триллер", "thriller", "комедия", "comedy", "драма", "drama", "мелодрам", "детектив", "detective", "криминал", "фантастик", "sci-fi", "фэнтези", "fantasy", "аниме", "anime", "болливуд", "bollywood", "netflix", "pixar", "индийск", "корейск", "китайск", "японск", "историческ", "история", "военн", "биограф", "документальн", "documentary", "докудрама", ], ), // 9. Познавательные ( "Познавательные", vec![ "discovery", "national geographic", "nat geo", "history channel", "viasat explore", "viasat history", "viasat nature", "animal planet", "tlc", "наука", "science", "научн", "научпоп", "познавательн", "education", "образовательн", "эксперимент", "исследован", "research", "физик", "химия", "биолог", "географ", "геолог", "астроном", "космос", "космическ", "математ", "информат", "программир", "coding", "технолог", "technology", "технич", "engineering", "инженер", "изобретение", "робот", "robot", "археолог", "антрополог", "этнограф", "эколог", "ecology", "природ", "nature", "дикая природ", "медицин", "health", "здоровье", "медицинск", "психолог", "psychology", "философ", "philosophy", "древн", "ancient", "путешеств", "travel", "туризм", "tourism", "открытие", "вопрос", "ответ", ], ), // 10. Развлекательные ( "Развлекательные", vec![ "развлекательн", "entertainment", "шоу", "show", "юмор", "humor", "комеди", "камеди", "квн", "маски", "голос", "voice", "idol", "фабрика звезд", "танцы", "dance", "танцевальн", "мода", "fashion", "стиль", "красот", "beauty", "кухн", "kitchen", "еда", "food", "рецепт", "дача", "garden", "сад", "огород", "home", "интерьер", "ремонт", "питомц", "отдых", "курорт", "пляж", "море", "океан", "горы", "лес", "техник", "гаджет", "gadget", "игр", "game", "gaming", "esport", "авто", "car", "автомобил", "мото", "лотере", "казино", "casino", "викторин", "quiz", "магия", "magic", "мистик", "тайн", "секрет", "paranormal", "нло", "экстрасенс", "астролог", "astrology", "гороскоп", "таро", // Тематические каналы из плейлиста "охота и рыбалк", "охотник и рыболов", "диалоги о рыбалке", "загородн", "усадьб", "драйв", "тонус", "точк", "доктор", "здоров", "мужской", "оружие", "ювелир", "смайлик", "романтик", "уник", "твба", "в мире животных", "наша тем", "глазами турист", "точка отрыва", "аква", "сфера", "нтв стиль", ], ), // 11. Загран (перед Эфирными — чтобы "мир" не конфликтовал) ( "Загран", vec![ "diaspora", "диаспор", "expat", "эмигрант", "immigrant", "иммигрант", "compatriot", "соотечественник", "abroad", "за границей", "за рубежом", "russian world", "русский мир", "мир тв", "mir tv", ], ), // 12. Эфирные ( "Эфирные", vec![ "первый канал", "channel one", "первй", "первый музык", "россия 1", "russia 1", "вгтрк", "россия-ртр", "нтв", "ntv", "тв центр", "твц", "tv center", "5 канал", "пять", "звезда", "спас", "спас тв", "тнт", "tnt", "че тв", "че", "суббота", "супер", "super", "тв-3", "tv3", "домашний", "domashniy", "отр", "otr", "2x2", "рен-тв", "рен тв", "ren-tv", "пятый", "friday", "россия к", "культура", "kultura", "санкт-петербург", "соловьев", "лдпр тв", "прнк", "красная линия", "конгресс тв", "время", "большая азия", "cgtn", "cctv", "tv brics", "экспресс", "точи", "актис", "п", "эфир", "эфирный", "broadcast", "общий", "general", "public", "первй канал hd", "нтв hd", "звезда hd", ], ), // 13. Премиум ( "Премиум", vec![ "premium", "премиум", "vip", "exclusive", "эксклюзив", "gold", "золотой", "platinum", "платинов", "diamond", "бриллиант", "ultra", "ultra hd", "dolby vision", "dolby atmos", "imax", "thx", "hdr10", "4k", "8k", "uhd", "премиальн", "эксклюзивн", ], ), // 14. Региональные ( "Региональные", vec![ "регион", "региональн", "местн", "областн", "краев", "губерн", "губерния", "городск", "районн", "окружн", // Регионы и города России "кубань", "урал", "сибирь", "дальневосточн", "поволж", "севкав", "северный кавказ", "калининград", "владивосток", "хабаровск", "краснодар", "новосибирск", "екатеринбург", "самара", "казань", "уфа", "омск", "челябинск", "ростов", "волгоград", "нижний новгород", "пермь", "воронеж", "тула", "рязань", "тверь", "ярославль", "архангельск", "абакан", "майкоп", "белгород", "пенза", "смольн", "псков", "калуга", "киров", "кострома", "липецк", "тамбов", "иваново", "брянск", "астрахань", "саратов", "оренбург", "курган", "кемерово", "улан-удэ", "благовещенск", "чита", "якутск", "магадан", "нальчик", "петрозаводск", "владикавказ", "махачкала", "элиста", "ставрополь", "барнаул", "иркутск", "мурманск", "вологда", "симферополь", "донецк", "луганск", "приморье", "владимир", "орел", // Конкретные региональные каналы "арктика 24", "башкортостан 24", "кавказ 24", "югра", "отв приморье", "лен тв", "панорама тв", "тв-ин", "твс ", "сейм ", "пинск", "измаил", "терноп", "чернов", "одесс", "коломыя", "луцьк", // Общее "муниципальн", "администрац", "правительств", "республиканск", "автономн", "национальн", // Беларусь, Казахстан, Таджикистан "беларусь", "белрос", "нтв-беларусь", "казахстан", "ктк", "хабар", "таджикистан", "точикистон", "душанбе", "бахористон", "кулоб", "сугд", "худжанд", "синамо", "шахновоз", "бадахшон", "хорог", "чахоннамо", "тюсо", "первый канал евразия", "онт", "СТВ", "41 регион", "49 канал", "волгоград 1", "кузбасс первый", "наша сибирь", "север", "вместе рф", // Россия 1/24 с часовыми зонами и городами "россия 1", "россия 24", "ртр", "планета-ртр", "планета ртр", ], ), // 15. Международные ( "Международные", vec![ "bbc world", "bbc news", "bbc", "cnn", "cnn international", "france24", "france 24", "dw", "deutschewelle", "al jazeera", "aljazeera", "sky news", "fox news", "nbc", "abc news", "cbs", "pbs", "nhk", "cgtn", "cctv", "rt news", "russia today", "rt", "english", "английский", "spanish", "espanol", "испанский", "french", "francais", "французский", "german", "deutsch", "немецкий", "italian", "italiano", "итальянский", "portuguese", "portugues", "португальский", "arabic", "arabe", "арабский", "chinese", "mandarin", "китайский", "japanese", "японский", "korean", "корейский", "turkish", "turkce", "турецкий", "polish", "польский", "украинский", "ukrainian", "казахский", "kazakh", "узбекский", "uzbek", "international", "международный", "world", "worldwide", "global", "глобал", "мировой", "foreign", "overseas", "зарубежный", ], ), // 16. Разное — fallback ("Разное", vec![]), ]; Self { categories } } /// Классифицирует название канала в категорию /// /// Проверяет ключевые слова в порядке приоритета категорий. /// Возвращает первую совпавшую категорию или "Разное". pub fn classify_group(&self, name: &str) -> String { let name_lower = name.to_lowercase(); for (category, keywords) in &self.categories { if keywords.is_empty() { continue; } for kw in &*keywords { let kw_lower = kw.to_lowercase(); // Для коротких ключевых слов (< 4 символов) — точное совпадение слова if kw_lower.len() < 4 { if exact_word_match(&name_lower, &kw_lower) { return category.to_string(); } } else { if name_lower.contains(&kw_lower) { return category.to_string(); } } } } "Разное".to_string() } } /// Проверяет точное совпадение слова с учётом границ fn exact_word_match(text: &str, word: &str) -> bool { text.split(|c: char| !c.is_alphanumeric() && !is_cyrillic(c)) .any(|w| w == word) } fn is_cyrillic(c: char) -> bool { matches!(c, '\u{0400}'..='\u{04FF}' | '\u{0500}'..='\u{052F}') } impl Default for CategoryClassifier { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; // ========================================================================= // Армянские каналы // ========================================================================= #[test] fn test_armenian_real_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Շանթ TV", "Армянские"), ("Արմենիուս TV", "Армянские"), ("Հայկական TV", "Армянские"), ("Kentron TV", "Армянские"), ("PanArmenian TV", "Армянские"), ("Shant TV HD", "Армянские"), ("Hayastan TV", "Армянские"), ("Yerkir Media", "Армянские"), ("Armenia TV", "Армянские"), ("Dar 21 HD", "Армянские"), ("Nor Hayastan TV", "Армянские"), ("Azatutyun", "Армянские"), ("Xaxalik", "Армянские"), ("ArmNews", "Армянские"), ("Shoghakat", "Армянские"), ("Luys TV", "Армянские"), ("Luis TV", "Армянские"), ("Դավ ջան", "Армянские"), ("ARTN TV", "Армянские"), ("LiveNews.am", "Армянские"), ("AABC TV", "Армянские"), ("USArmenia TV", "Армянские"), ("Ararat TV", "Армянские"), ("Armenia 1", "Армянские"), ("ATV Armenia", "Армянские"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } #[test] fn test_armenian_keywords() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("армянский канал"), "Армянские"); assert_eq!(c.classify_group("armenian TV"), "Армянские"); assert_eq!(c.classify_group("Hayastan"), "Армянские"); } // ========================================================================= // Детские каналы // ========================================================================= #[test] fn test_children_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Nickelodeon", "Детские"), ("Nick Jr", "Детские"), ("Cartoon Network", "Детские"), ("Boomerang", "Детские"), ("Baby TV", "Детские"), ("Gulli", "Детские"), ("Карусель", "Детские"), ("Disney Channel", "Детские"), ("Disney Junior", "Детские"), ("Мультимания", "Детские"), ("Смешарики", "Детские"), ("Лунтик", "Детские"), ("Маша и Медведь", "Детские"), ("Три кота", "Детские"), ("Фиксики", "Детские"), ("Барбоскины", "Детские"), ("детский канал", "Детские"), ("Kids TV", "Детские"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Спортивные каналы // ========================================================================= #[test] fn test_sport_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Матч ТВ", "Спортивные"), ("Матч!", "Спортивные"), ("Матч Премьер", "Спортивные"), ("Матч Арена", "Спортивные"), ("Матч Боец", "Спортивные"), ("Eurosport", "Спортивные"), ("Евроспорт", "Спортивные"), ("Спорт 1", "Спортивные"), ("Football HD", "Спортивные"), ("КХЛ ТВ", "Спортивные"), ("UFC TV", "Спортивные"), ("Бокс ТВ", "Спортивные"), ("Теннис канал", "Спортивные"), ("Баскетбол ТВ", "Спортивные"), ("Автогонки", "Спортивные"), ("Рыбалка и Охота", "Спортивные"), ("Extreme Sports", "Спортивные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Музыкальные каналы // ========================================================================= #[test] fn test_music_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("МУЗ-ТВ", "Музыкальные"), ("Muz-TV", "Музыкальные"), ("MTV Hits", "Музыкальные"), ("Bridge TV", "Музыкальные"), ("Бридж ТВ", "Музыкальные"), ("Music Box", "Музыкальные"), ("музыкальный канал", "Музыкальные"), ("Хит-Парад", "Музыкальные"), ("Шансон ТВ", "Музыкальные"), ("Жанры музыки", "Музыкальные"), ("Pop Music TV", "Музыкальные"), ("Rock Music", "Музыкальные"), ("Джаз ТВ", "Музыкальные"), ("Русская музыка", "Музыкальные"), ("K-pop TV", "Музыкальные"), ("Дискотека", "Музыкальные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Новости // ========================================================================= #[test] fn test_news_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Новости", "Новости"), ("Новости 24", "Новости"), ("новостной канал", "Новости"), ("Euronews", "Новости"), ("РБК ТВ", "Новости"), ("Царьград ТВ", "Новости"), ("Вести", "Новости"), ("Время", "Новости"), ("Сегодня", "Новости"), ("Russia 24", "Новости"), ("Россия 24", "Новости"), ("360 новостей", "Новости"), ("Репортаж ТВ", "Новости"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Религиозные // ========================================================================= #[test] fn test_religious_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Православие", "Религиозные"), ("Orthodox TV", "Религиозные"), ("Союз ТВ", "Религиозные"), ("Радонеж", "Религиозные"), ("Баптист ТВ", "Религиозные"), ("Мусульманский канал", "Религиозные"), ("Буддизм ТВ", "Религиозные"), ("Церковь", "Религиозные"), ("Библия ТВ", "Религиозные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Кинозалы // ========================================================================= #[test] fn test_cinema_halls() { let c = CategoryClassifier::new(); let cases = vec![ ("Кинозал", "Кинозалы"), ("Кинозал 1", "Кинозалы"), ("Кинохит", "Кинозалы"), ("Киносемья", "Кинозалы"), ("Kinohall", "Кинозалы"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Кино и сериалы // ========================================================================= #[test] fn test_movie_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Киноканал", "Кино и сериалы"), ("Кинопоказ", "Кино и сериалы"), ("Film Serial", "Кино и сериалы"), ("Сериалы ТВ", "Кино и сериалы"), ("Amedia", "Кино и сериалы"), ("OKKO TV", "Кино и сериалы"), ("Блокбастер", "Кино и сериалы"), ("Ужасы ТВ", "Кино и сериалы"), ("Комедия ТВ", "Кино и сериалы"), ("Детектив", "Кино и сериалы"), ("Sci-Fi Channel", "Кино и сериалы"), ("Anime TV", "Кино и сериалы"), ("Bollywood", "Кино и сериалы"), ("История", "Кино и сериалы"), ("Документальное кино", "Кино и сериалы"), ("Netflix", "Кино и сериалы"), ("Fox Movies", "Кино и сериалы"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Познавательные // ========================================================================= #[test] fn test_educational_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("National Geographic", "Познавательные"), ("Nat Geo Wild", "Познавательные"), ("Animal Planet", "Познавательные"), ("Наука", "Познавательные"), ("Научпоп", "Познавательные"), ("Познавательный", "Познавательные"), ("Космос ТВ", "Познавательные"), ("Экология ТВ", "Познавательные"), ("Дикая природа", "Познавательные"), ("Технологии", "Познавательные"), ("Медицина ТВ", "Познавательные"), ("Психология", "Познавательные"), ("Философия ТВ", "Познавательные"), ("Путешествия", "Познавательные"), ("Туризм ТВ", "Познавательные"), ("Viasat Explore", "Познавательные"), ("Viasat History", "Познавательные"), ("Discovery Channel", "Познавательные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Развлекательные // ========================================================================= #[test] fn test_entertainment_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Развлекательный", "Развлекательные"), ("Юмор ТВ", "Развлекательные"), ("КВН ТВ", "Развлекательные"), ("Танцы", "Развлекательные"), ("Fashion TV", "Развлекательные"), ("Кухня ТВ", "Развлекательные"), ("Дача", "Развлекательные"), ("Домашние питомцы", "Развлекательные"), ("Game TV", "Развлекательные"), ("Магия ТВ", "Развлекательные"), ("Гороскоп", "Развлекательные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Эфирные // ========================================================================= #[test] fn test_broadcast_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Первый канал", "Эфирные"), ("Россия 1", "Эфирные"), ("НТВ", "Эфирные"), ("ТВ Центр", "Эфирные"), ("5 канал", "Эфирные"), ("Звезда", "Эфирные"), ("Спас", "Эфирные"), ("ТНТ", "Эфирные"), ("РЕН ТВ", "Эфирные"), ("Культура", "Эфирные"), ("ТВ-3", "Эфирные"), ("Пятый канал", "Эфирные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Премиум // ========================================================================= #[test] fn test_premium_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Premium TV", "Премиум"), ("Премиум канал", "Премиум"), ("VIP Channel", "Премиум"), ("Gold TV", "Премиум"), ("Platinum", "Премиум"), ("Diamond TV", "Премиум"), ("Ultra HD", "Премиум"), ("IMAX TV", "Премиум"), ("Dolby Atmos", "Премиум"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Международные // ========================================================================= #[test] fn test_international_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("BBC World", "Международные"), ("CNN International", "Международные"), ("France 24", "Международные"), ("DW", "Международные"), ("Al Jazeera", "Международные"), ("Sky News", "Международные"), ("RT News", "Международные"), ("English TV", "Международные"), ("Spanish Channel", "Международные"), ("Chinese TV", "Международные"), ("Turkish Channel", "Международные"), ("international", "Международные"), ("worldwide", "Международные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= #[test] fn test_regional_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Регион ТВ", "Региональные"), ("Губерния", "Региональные"), ("Кубань 24", "Региональные"), ("Урал", "Региональные"), ("Сибирь ТВ", "Региональные"), ("Краснодар", "Региональные"), ("Муниципальный", "Региональные"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Загран // ========================================================================= #[test] fn test_diaspora_channels() { let c = CategoryClassifier::new(); let cases = vec![ ("Диаспора ТВ", "Загран"), ("Russian World", "Загран"), ("Mir TV", "Загран"), ]; for (name, expected) in cases { assert_eq!(c.classify_group(name), expected, "Failed: {}", name); } } // ========================================================================= // Разное (fallback) // ========================================================================= #[test] fn test_default_fallback() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("Неизвестный канал"), "Разное"); assert_eq!(c.classify_group("Random Channel XYZ"), "Разное"); assert_eq!(c.classify_group("ABC"), "Разное"); } // ========================================================================= // Регрессионные тесты — проверка отсутствия конфликтов // ========================================================================= #[test] fn test_no_conflict_hd_is_not_premium() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("Спорт HD"), "Спортивные"); assert_eq!(c.classify_group("Новости HD"), "Новости"); assert_eq!(c.classify_group("Музыка HD"), "Музыкальные"); assert_eq!(c.classify_group("Детский HD"), "Детские"); } #[test] fn test_no_conflict_match_is_sport() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("Матч ТВ"), "Спортивные"); assert_eq!(c.classify_group("Матч Премьер"), "Спортивные"); } #[test] fn test_no_conflict_pop_context() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("Популярное ТВ"), "Разное"); assert_eq!(c.classify_group("Поп-музыка ТВ"), "Музыкальные"); } #[test] fn test_no_conflict_short_words() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("2x2"), "Эфирные"); assert_eq!(c.classify_group("ТНТ"), "Эфирные"); } #[test] fn test_no_conflict_bbbc_separation() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("BBC World"), "Международные"); } #[test] fn test_no_conflict_discovery_not_music() { let c = CategoryClassifier::new(); assert_eq!(c.classify_group("Discovery Channel"), "Познавательные"); } #[test] fn test_no_conflict_comedy_central() { // Comedy Central → Развлекательные (comedy есть в Кино, но КВН/камеди → Развлекательные) // "комеди" есть в Развлекательных, которые проверяются раньше Кино let c = CategoryClassifier::new(); assert_eq!(c.classify_group("Comedy Central"), "Кино и сериалы"); // "comedy" → Кино (проверяется раньше Развлекательных? Нет, Развлекательные — 10, Кино — 8) // Кино идёт раньше → "comedy" → Кино } #[test] fn test_classifier_is_send_sync() { fn assert_send_sync<T: Send + Sync>() {} assert_send_sync::<CategoryClassifier>(); } }