use crate::model::{PlaylistItemType, SearchRequest, XtreamCluster}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::rc::Rc; #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] pub struct PlaylistRequestXtream { pub username: String, pub password: String, pub url: String, } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] pub struct PlaylistRequestM3u { pub url: String, } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] pub enum PlaylistRequest { Target(u16), Input(u16), CustomXtream(PlaylistRequestXtream), CustomM3u(PlaylistRequestM3u) } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct CommonPlaylistItem { pub virtual_id: u32, pub provider_id: String, pub name: String, pub chno: String, pub logo: String, pub logo_small: String, pub group: String, pub title: String, pub parent_code: String, pub audio_track: String, pub time_shift: String, pub rec: String, pub url: String, pub input_name: String, pub item_type: PlaylistItemType, #[serde(skip_serializing_if = "Option::is_none")] pub epg_channel_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub xtream_cluster: Option, #[serde(skip_serializing_if = "Option::is_none")] pub additional_properties: Option, #[serde(skip_serializing_if = "Option::is_none")] pub category_id: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct PlaylistResponseGroup { pub id: u32, pub title: String, pub channels: Vec, pub xtream_cluster: XtreamCluster, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct UiPlaylistGroup { pub id: u32, pub title: String, pub channels: Vec>, pub xtream_cluster: XtreamCluster, } impl From for UiPlaylistGroup { fn from(response: PlaylistResponseGroup) -> Self { Self { id: response.id, title: response.title, channels: response.channels.into_iter().map(Rc::new).collect(), xtream_cluster: response.xtream_cluster, } } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct PlaylistCategoriesResponse { #[serde(skip_serializing_if = "Option::is_none")] pub live: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub vod: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub series: Option>, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct UiPlaylistCategories { #[serde(skip_serializing_if = "Option::is_none")] pub live: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub vod: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub series: Option>>, } impl From for UiPlaylistCategories { fn from(response: PlaylistCategoriesResponse) -> Self { Self { live: response.live.map(|groups| groups.into_iter().map(Into::into).map(Rc::new).collect()), vod: response.vod.map(|groups| groups.into_iter().map(Into::into).map(Rc::new).collect()), series: response.series.map(|groups| groups.into_iter().map(Into::into).map(Rc::new).collect()), } } } fn filter_channels( groups: &Option>>, text: &str, ) -> Option>> { // normalize search text (lowercase) let text = text.to_lowercase(); groups.as_ref().map(|gs| { gs.iter() .filter_map(|group| { let title_lower = group.title.to_lowercase(); if title_lower.contains(&text) { return Some(Rc::clone(group)); } let filtered_channels: Vec> = group .channels .iter() .filter(|c| { c.title.to_lowercase().contains(&text) || c.name.to_lowercase().contains(&text) }) .cloned() .collect(); if filtered_channels.is_empty() { None } else { Some(Rc::new(UiPlaylistGroup { id: group.id, title: group.title.clone(), channels: filtered_channels, xtream_cluster: group.xtream_cluster, })) } }) .collect::>() }) } fn filter_channels_re(groups: &Option>>, regex: &Regex) -> Option>> { groups.as_ref().map(|gs| { gs.iter() .filter_map(|group| { if regex.is_match(&group.title) { return Some(Rc::clone(group)); } let filtered_channels: Vec> = group .channels .iter() .filter(|c| regex.is_match(&c.title) || regex.is_match(&c.name)) .cloned() .collect(); if filtered_channels.is_empty() { None } else { Some(Rc::new(UiPlaylistGroup { id: group.id, title: group.title.clone(), channels: filtered_channels, xtream_cluster: group.xtream_cluster, })) } }) .collect::>() }) } fn build_result(live: Option>>, vod: Option>>, series: Option>>) -> Option { if live.is_none() && vod.is_none() && series.is_none() { None } else { Some(UiPlaylistCategories { live, vod, series, }) } } impl UiPlaylistCategories { pub fn filter(&self, search_req: &SearchRequest) -> Option { match search_req { SearchRequest::Clear => None, SearchRequest::Text(text, _search_fields) => { let text_lc = text.to_lowercase(); let live = filter_channels(&self.live, &text_lc); let video = filter_channels(&self.vod, &text_lc); let series = filter_channels(&self.series, &text_lc); build_result(live, video, series) } SearchRequest::Regexp(text, _search_fields) => { if let Ok(regex) = Regex::new(text) { let live = filter_channels_re(&self.live, ®ex); let video = filter_channels_re(&self.vod, ®ex); let series = filter_channels_re(&self.series, ®ex); build_result(live, video, series) } else { None } } } } }