diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index e89f7ed4d..7b744e4f3 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -4,6 +4,36 @@ use actix_web::{HttpRequest, HttpResponse, web}; use crate::api::api_model::{AppState, UserApiRequest}; use crate::model::api_proxy::{UserCredentials}; use crate::model::config::{ConfigTarget}; +use url::Url; + +pub (crate) struct M3uUrlInfo { + pub base_url: String, + pub username: String, + pub password: String, +} + +pub (crate) fn parse_m3u_url(url: &str) -> Option { + if let Ok(url) = Url::parse(url) { + let base_url = url.origin().ascii_serialization(); + let mut username = None; + let mut password = None; + for (key, value) in url.query_pairs() { + if key.eq("username") { + username = Some(value.into_owned()); + } else if key.eq("password") { + password = Some(value.into_owned()); + } + } + if username.is_some() || password.is_some() { + return Some(M3uUrlInfo { + base_url, + username: username.as_ref().unwrap().to_owned(), + password: username.as_ref().unwrap().to_owned(), + }); + } + } + None +} pub(crate) async fn serve_file(file_path: &Path, req: &HttpRequest) -> HttpResponse { if file_path.exists() { diff --git a/src/api/xmltv_api.rs b/src/api/xmltv_api.rs index 3c4b29eda..c6037f6bd 100644 --- a/src/api/xmltv_api.rs +++ b/src/api/xmltv_api.rs @@ -51,28 +51,31 @@ async fn xmltv_api( // If no epg_url is provided for input, we did not process the xmltv for our channels. // We are now delivering the original untouched xmltv. // If you want to use xmltv then provide the url in the config to filter unnecessary content. - // If you have multiple xtream sources, the first one will be used for epg + // If you have multiple xtream sources, no response because of mapped ids + // if you want epg for multi xtream input, then provide epg_url. let target_name = &target.name; if let Some(inputs) = _app_state.config.get_input_for_target(target_name, &InputType::Xtream) { - if let Some(&input) = inputs.first() { - let epg_url = input.epg_url.as_ref().map_or("".to_string(), |s| s.to_owned()); - let api_url = if epg_url.is_empty() { - format!("{}/xmltv.php?username={}&password={}", - input.url.as_str(), - input.username.as_ref().unwrap_or(&"".to_string()).as_str(), - input.password.as_ref().unwrap_or(&"".to_string()).as_str(), - ) - } else { epg_url.to_string() }; - if let Ok(url) = Url::parse(&api_url) { - if user.proxy == ProxyType::Redirect { - debug!("Redirecting epg request to {}", api_url); - return HttpResponse::Found().insert_header(("Location", api_url)).finish(); - } - let client = request_utils::get_client_request(input, url, None); - if let Ok(response) = client.send().await { - if response.status().is_success() { - if let Ok(content) = response.text().await { - return HttpResponse::Ok().content_type(mime::TEXT_XML).body(content); + if inputs.len() == 1 { + if let Some(&input) = inputs.first() { + let epg_url = input.epg_url.as_ref().map_or("".to_string(), |s| s.to_owned()); + let api_url = if epg_url.is_empty() { + format!("{}/xmltv.php?username={}&password={}", + input.url.as_str(), + input.username.as_ref().unwrap_or(&"".to_string()).as_str(), + input.password.as_ref().unwrap_or(&"".to_string()).as_str(), + ) + } else { epg_url.to_string() }; + if let Ok(url) = Url::parse(&api_url) { + if user.proxy == ProxyType::Redirect { + debug!("Redirecting epg request to {}", api_url); + return HttpResponse::Found().insert_header(("Location", api_url)).finish(); + } + let client = request_utils::get_client_request(input, url, None); + if let Ok(response) = client.send().await { + if response.status().is_success() { + if let Ok(content) = response.text().await { + return HttpResponse::Ok().content_type(mime::TEXT_XML).body(content); + } } } } diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index 4d272f9a7..2cdebab61 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -11,6 +11,7 @@ use url::Url; use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, serve_file}; use crate::api::api_model::{AppState, UserApiRequest, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo}; +use crate::api::api_utils; use crate::model::api_proxy::{ProxyType, UserCredentials}; use crate::model::config::{Config, ConfigInput, InputType}; use crate::model::config::{TargetType}; @@ -18,35 +19,6 @@ use crate::model::playlist::XtreamCluster; use crate::repository::xtream_repository; use crate::utils::{json_utils, request_utils}; -struct M3uUrlInfo { - pub base_url: String, - pub username: String, - pub password: String, -} - -fn parse_m3u_url(url: &str) -> Option { - if let Ok(url) = Url::parse(url) { - let base_url = url.origin().ascii_serialization(); - let mut username = None; - let mut password = None; - for (key, value) in url.query_pairs() { - if key.eq("username") { - username = Some(value.into_owned()); - } else if key.eq("password") { - password = Some(value.into_owned()); - } - } - if username.is_some() || password.is_some() { - return Some(M3uUrlInfo { - base_url, - username: username.as_ref().unwrap().to_owned(), - password: username.as_ref().unwrap().to_owned(), - }); - } - } - None -} - pub(crate) async fn serve_query(file_path: &Path, filter: &HashMap<&str, &str>) -> HttpResponse { let filtered = json_utils::filter_json_file(file_path, filter); HttpResponse::Ok().json(filtered) @@ -55,7 +27,7 @@ pub(crate) async fn serve_query(file_path: &Path, filter: &HashMap<&str, &str>) fn get_xtream_player_api_action_url(input: &ConfigInput, action: &str) -> Option { match input.input_type { InputType::M3u => { - match parse_m3u_url(input.url.as_str()) { + match api_utils::parse_m3u_url(input.url.as_str()) { None => None, Some(m3u_url_info) => Some( format!("{}/player_api.php?username={}&password={}&action={}", @@ -89,7 +61,7 @@ fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: &XtreamCluster, fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &str, action_path: &str) -> Option { let ctx_path = if context.is_empty() { "".to_string() } else { format!("{}/", context) }; match input.input_type { - InputType::M3u => match parse_m3u_url(input.url.as_str()) { + InputType::M3u => match api_utils::parse_m3u_url(input.url.as_str()) { None => None, Some(m3u_url_info) => Some( format!("{}/{}{}/{}/{}", @@ -174,7 +146,6 @@ async fn xtream_player_api_stream( if target.has_output(&TargetType::Xtream) { let mut stream_id = action_path.to_owned(); let mut input: Option<&ConfigInput> = None; - let (action_stream_id, action_ext) = separate_number_and_rest(action_path); if let Ok(num) = action_stream_id.trim().parse() { let (xtream_id, cfg_input) = get_xtream_mapped_id_and_input_for_stream_id(_app_state, target_name, num); @@ -300,13 +271,11 @@ fn get_xtream_mapped_id_and_input_for_stream_id<'a>(app_state: &'a AppState, tar (stream_id, None) } -// TODO use u32 or i32 ?? async fn xtream_get_stream_info(app_state: &AppState, target_name: &str, stream_id: i32, cluster: &XtreamCluster) -> Result { - let (xtream_id, input) = get_xtream_mapped_id_and_input_for_stream_id(app_state, target_name, stream_id); if let Some(target_input) = input { - if let Ok(content) = xtream_repository::xtream_get_stored_stream_info(app_state, target_name, xtream_id, cluster, target_input).await { + if let Ok(content) = xtream_repository::xtream_get_stored_stream_info(app_state, target_name, stream_id, cluster, target_input).await { return Ok(content); } @@ -341,7 +310,7 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &UserCreden Err(_) => return HttpResponse::BadRequest().finish() }; - if user.proxy == ProxyType::Redirect { + if user.proxy == ProxyType::Redirect && !app_state.config.is_multi_xtream_input(target_name) { let (xtream_id, input) = get_xtream_mapped_id_and_input_for_stream_id(app_state, target_name, xtream_stream_id); if let Some(target_input) = input { if let Some(info_url) = get_xtream_player_api_info_url(target_input, cluster, xtream_id) { diff --git a/src/model/config.rs b/src/model/config.rs index eaa6cc565..b69263255 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -35,7 +35,6 @@ macro_rules! valid_property { }}; } - pub(crate) fn default_as_true() -> bool { true } pub(crate) fn default_as_false() -> bool { false } @@ -425,6 +424,7 @@ pub(crate) struct ConfigSource { impl ConfigSource { pub(crate) fn prepare(&mut self, index: u16) -> Result { handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, self.inputs.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+(idx as u16)))); + self._multi_xtream_input = self.inputs.iter().filter(|i| i.input_type == InputType::Xtream).count() > 1; Ok(index + (self.inputs.len() as u16)) } @@ -740,7 +740,7 @@ impl Config { None } - pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(UserCredentials, &ConfigTarget)> { + pub(crate) fn get_target_for_user(&self, username: &str, password: &str) -> Option<(UserCredentials, &ConfigTarget)> { match self._api_proxy.read().unwrap().as_ref() { Some(api_proxy) => { self._get_target_for_user(api_proxy.get_target_name(username, password)) @@ -749,7 +749,7 @@ impl Config { } } - pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(UserCredentials, &ConfigTarget)> { + pub(crate) fn get_target_for_user_by_token(&self, token: &str) -> Option<(UserCredentials, &ConfigTarget)> { match self._api_proxy.read().unwrap().as_ref() { Some(api_proxy) => { self._get_target_for_user(api_proxy.get_target_name_by_token(token)) @@ -758,7 +758,7 @@ impl Config { } } - pub fn get_input_by_id(&self, input_id: &u16) -> Option { + pub(crate) fn get_input_by_id(&self, input_id: &u16) -> Option { for source in &self.sources { for input in &source.inputs { if input.id == *input_id { @@ -769,7 +769,18 @@ impl Config { None } - pub fn set_mappings(&mut self, mappings: Option) -> Result<(), M3uFilterError> { + pub(crate) fn is_multi_xtream_input(&self, target_name: &str) -> bool { + for source in &self.sources { + for target in &source.targets { + if target_name.eq_ignore_ascii_case(&target.name) { + return source._multi_xtream_input; + } + } + } + false + } + + pub(crate) fn set_mappings(&mut self, mappings: Option) -> Result<(), M3uFilterError> { if let Some(mapping_list) = mappings { for source in &mut self.sources { for target in &mut source.targets { @@ -789,7 +800,7 @@ impl Config { Ok(()) } - pub fn prepare(&mut self) -> Result<(), M3uFilterError> { + pub(crate) fn prepare(&mut self) -> Result<(), M3uFilterError> { self.working_dir = file_utils::get_working_path(&self.working_dir); if self.backup_dir.is_none() { self.backup_dir = Some(PathBuf::from(&self.working_dir).join(".backup").into_os_string().to_string_lossy().to_string()); diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index 8109f046c..ee7c04af3 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -244,39 +244,26 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option // if the group names are changed, restructure channels to the right groups // we use - let mut max_group_id = 0; let mut new_groups: Vec = Vec::new(); + let mut grp_id: u32 = 0; for playlist_group in new_playlist { - let mut group_id_used = false; for channel in &playlist_group.channels { let cluster = &channel.header.borrow().xtream_cluster; let title = &channel.header.borrow().group; match new_groups.iter_mut().find(|x| *x.title == **title) { Some(grp) => grp.channels.push(channel.clone()), _ => { - let new_group_id = if group_id_used { - 0 - } else if *title == playlist_group.title { - group_id_used = true; - max_group_id = max_group_id.max(playlist_group.id); - playlist_group.id - } else { - 0 - }; + grp_id += 1; new_groups.push(PlaylistGroup { - id: new_group_id, + id: grp_id, title: Rc::clone(title), channels: vec![channel.clone()], - xtream_cluster: cluster.clone(), + xtream_cluster: cluster.clone() }) } } } } - new_groups.iter_mut().filter(|g| g.id == 0).for_each(|grp| { - max_group_id += 1; - grp.id = max_group_id; - }); Some(new_groups) } else { None diff --git a/src/processing/xtream_parser.rs b/src/processing/xtream_parser.rs index a55b9e79c..2e49fe2d5 100644 --- a/src/processing/xtream_parser.rs +++ b/src/processing/xtream_parser.rs @@ -143,7 +143,7 @@ pub(crate) fn parse_xtream(input: &ConfigInput, Ok(Some(group_map.values().map(|category| { let cat = category.borrow(); PlaylistGroup { - id: 0, + id: cat.category_id.parse::().unwrap_or(0), xtream_cluster: xtream_cluster.clone(), title: Rc::clone(&cat.category_name), channels: cat.channels.clone() diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index 7b1e44a82..301f87682 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -13,7 +13,7 @@ use crate::model::playlist::{PlaylistGroup, PlaylistItemHeader, PlaylistItemType use crate::{create_m3u_filter_error_result}; use crate::api::api_model::AppState; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::model_xtream::MultiXtreamMapping; +use crate::model::xtream::MultiXtreamMapping; use crate::utils::file_utils; use crate::utils::json_utils::iter_json_array; @@ -450,19 +450,25 @@ fn get_id_mapping_path(path: &Path) -> PathBuf { path.join("id_mapping.db") } -pub(crate) fn write_xtream_mapping(mappings: &[MultiXtreamMapping], config: &Config, target_name: &str) -> io::Result<()> { +pub(crate) fn write_xtream_mapping(mappings: &[MultiXtreamMapping], config: &Config, target_name: &str) -> Result<(), M3uFilterError> { if let Some(path) = get_xtream_storage_path(config, target_name) { - let mut file = File::create(get_id_mapping_path(&path))?; + if fs::create_dir_all(&path).is_err() { + let msg = format!("Failed to save, can't create directory {}", &path.to_str().unwrap()); + return Err(M3uFilterError::new(M3uFilterErrorKind::Notify, msg)); + } + + let err_map = |e: Error| M3uFilterError::new(M3uFilterErrorKind::Notify, e.to_string()); + let mut file = File::create(get_id_mapping_path(&path)).map_err(err_map)?; // We assume the mappings list is created with a counter as id // and id 1 means the 0 index. We write all the data and can calculate the offset inside the // file by (u32 size + u16 size) * index. for mapping in mappings { - file.write_all(&mapping.stream_id.to_le_bytes())?; - file.write_all(&mapping.input_id.to_le_bytes())?; + file.write_all(&mapping.stream_id.to_le_bytes()).map_err(err_map)?; + file.write_all(&mapping.input_id.to_le_bytes()).map_err(err_map)?; }; return Ok(()); } - Err(io::Error::new(ErrorKind::Other, format!("Failed to find the xtream storage path for {}", target_name))) + Err(M3uFilterError::new(M3uFilterErrorKind::Notify, format!("Failed to find the xtream storage path for {}", target_name))) } pub(crate) fn read_xtream_mapping(id: u32, config: &Config, target_name: &str) -> io::Result> {