diff --git a/src/api/endpoints/v1_api.rs b/src/api/endpoints/v1_api.rs index 6c1e0fb53..b3b8fc6c8 100644 --- a/src/api/endpoints/v1_api.rs +++ b/src/api/endpoints/v1_api.rs @@ -190,7 +190,7 @@ async fn get_playlist(client: Arc, cfg_input: Option<&ConfigInp match input.input_type { InputType::M3u => m3u::get_m3u_playlist(client, cfg, input, &cfg.working_dir).await, InputType::Xtream => xtream::get_xtream_playlist(client, input, &cfg.working_dir).await, - InputType::M3uBatch => (vec![], vec![]) + InputType::M3uBatch | InputType::XtreamBatch => (vec![], vec![]) }; if result.is_empty() { let error_strings: Vec = errors.iter().map(std::string::ToString::to_string).collect(); diff --git a/src/model/config.rs b/src/model/config.rs index eb8d57e36..d58c03ec4 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -52,7 +52,7 @@ macro_rules! valid_property { pub use valid_property; use crate::m3u_filter_error::{create_m3u_filter_error_result, handle_m3u_filter_error_result, handle_m3u_filter_error_result_list}; use crate::model::hdhomerun_config::HdHomeRunConfig; -use crate::utils::file::config_reader::resolve_env_var; +use crate::utils::file::config_reader::{csv_read_inputs, resolve_env_var}; use crate::utils::network::request::{get_credentials_from_url, get_credentials_from_url_str}; use crate::utils::string_utils::get_trimmed_string; @@ -601,12 +601,15 @@ pub enum InputType { Xtream, #[serde(rename = "m3u_batch")] M3uBatch, + #[serde(rename = "xtream_batch")] + XtreamBatch, } impl InputType { const M3U: &'static str = "m3u"; const XTREAM: &'static str = "xtream"; const M3U_BATCH: &'static str = "m3u_batch"; + const XTREAM_BATCH: &'static str = "xtream_batch"; } impl Display for InputType { @@ -615,6 +618,7 @@ impl Display for InputType { Self::M3u => Self::M3U, Self::Xtream => Self::XTREAM, Self::M3uBatch => Self::M3U_BATCH, + Self::XtreamBatch => Self::XTREAM_BATCH, }) } } @@ -684,7 +688,7 @@ impl InputUserInfo { macro_rules! check_input_credentials { ($this:ident, $input_type:expr) => { match $input_type { - InputType::M3u => { + InputType::M3u | InputType::M3uBatch => { if $this.username.is_some() || $this.password.is_some() { debug!("for input type m3u: username and password are ignored"); } @@ -694,16 +698,11 @@ macro_rules! check_input_credentials { $this.password = password; } } - InputType::Xtream => { + InputType::Xtream | InputType::XtreamBatch => { if $this.username.is_none() || $this.password.is_none() { return Err(info_err!("for input type xtream: username and password are mandatory".to_string())); } } - InputType::M3uBatch => { - if $this.username.is_some() || $this.password.is_some() { - debug!("for input type m3u_batch: username and password are ignored"); - } - } } }; } @@ -783,19 +782,21 @@ impl ConfigInput { #[allow(clippy::cast_possible_truncation)] pub fn prepare(&mut self, index: u16) -> Result { self.id = index; + self.check_url()?; + self.url = resolve_env_var(&self.url); + self.prepare_batch()?; + self.name = self.name.trim().to_string(); if self.name.is_empty() { return Err(info_err!("name for input is mandatory".to_string())); } - self.url = self.url.trim().to_string(); - if self.url.is_empty() { - return Err(info_err!("url for input is mandatory".to_string())); - } - self.url = resolve_env_var(&self.url); + self.username = get_trimmed_string(&self.username); self.password = get_trimmed_string(&self.password); check_input_credentials!(self, self.input_type); self.persist = get_trimmed_string(&self.persist); + + if let Some(aliases) = self.aliases.as_mut() { let input_type = &self.input_type; handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, aliases.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+1+(idx as u16), input_type))); @@ -803,6 +804,51 @@ impl ConfigInput { Ok(index + self.aliases.as_ref().map_or(0, std::vec::Vec::len) as u16) } + fn check_url(&mut self) -> Result<(), M3uFilterError> { + self.url = self.url.trim().to_string(); + if self.url.is_empty() { + return Err(info_err!("url for input is mandatory".to_string())); + } + Ok(()) + } + + fn prepare_batch(&mut self) -> Result<(), M3uFilterError> { + if self.input_type == InputType::M3uBatch || self.input_type == InputType::XtreamBatch { + let input_type = if self.input_type == InputType::M3uBatch { + InputType::M3u + } else { + InputType::Xtream + }; + + match csv_read_inputs(input_type.clone(), &self.url) { + Ok(mut batch_aliases) => { + if !batch_aliases.is_empty() { + if let Some(mut first) = batch_aliases.pop() { + self.username = first.username.take(); + self.password = first.password.take(); + self.url = first.url.trim().to_string(); + if self.name.is_empty() { + self.name = first.name.to_string(); + } + } + if !batch_aliases.is_empty() { + if let Some(aliases) = self.aliases.as_mut() { + aliases.extend(batch_aliases); + } else { + self.aliases = Some(batch_aliases); + } + } + } + } + Err(err) => { + return Err(M3uFilterError::new(M3uFilterErrorKind::Info, err.to_string())); + } + } + self.input_type = input_type; + } + Ok(()) + } + pub fn get_user_info(&self) -> Option { InputUserInfo::new(self.input_type.clone(), self.username.as_deref(), self.password.as_deref(), &self.url) } diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 3e4d29a7f..cf5c80c1f 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -317,7 +317,7 @@ async fn process_source(client: Arc, cfg: Arc, source_i let (mut playlistgroups, mut error_list) = match input.input_type { InputType::M3u => m3u::get_m3u_playlist(Arc::clone(&client), &cfg, input, &cfg.working_dir).await, InputType::Xtream => xtream::get_xtream_playlist(Arc::clone(&client), input, &cfg.working_dir).await, - InputType::M3uBatch => (vec![], vec![]) + InputType::M3uBatch | InputType::XtreamBatch => (vec![], vec![]) }; let (tvguide, mut tvguide_errors) = if error_list.is_empty() { epg::get_xmltv(Arc::clone(&client), &cfg, input, &cfg.working_dir).await diff --git a/src/utils/file/config_reader.rs b/src/utils/file/config_reader.rs index 6017ccfa5..d3167ad53 100644 --- a/src/utils/file/config_reader.rs +++ b/src/utils/file/config_reader.rs @@ -14,6 +14,7 @@ use std::io::{self, BufRead}; use std::path::PathBuf; use std::sync::LazyLock; use url::{Url}; +use crate::utils::network::request::get_credentials_from_url; pub fn read_mappings(args_mapping: Option, cfg: &mut Config) -> Result, M3uFilterError> { let mappings_file: String = args_mapping.unwrap_or_else(|| file_utils::get_default_mappings_path(cfg.t_config_path.as_str())); @@ -170,18 +171,32 @@ const FIELD_MAX_CON: &str = "max_connections"; const FIELD_PRIO: &str = "priority"; const FIELD_URL: &str = "url"; const FIELD_NAME: &str = "name"; +const FIELD_USERNAME: &str = "username"; +const FIELD_PASSWORD: &str = "password"; const FIELD_UNKNOWN: &str = "?"; fn csv_assign_mandatory_fields(alias: &mut ConfigInputAlias) { if alias.name.is_empty() && !alias.url.is_empty() { match Url::parse(alias.url.as_str()) { Ok(url) => { - let username = alias.username.as_ref().map(|s|s.as_str()).unwrap_or_default(); - let domain: Vec<&str> = url.domain().unwrap_or_default().split('.').collect(); - if domain.len() > 1 { - alias.name = format!("{}_{username}", domain[domain.len() - 2]); + let (username, password) = get_credentials_from_url(&url); + if username.is_none() || password.is_none() { + // xtream url + alias.url = url.origin().ascii_serialization().to_string(); } else { - alias.name = format!("{username}"); + // m3u url + alias.username = username; + alias.password = password; + } + + if alias.name.is_empty() { + let username = alias.username.as_ref().map(|s|s.as_str()).unwrap_or_default(); + let domain: Vec<&str> = url.domain().unwrap_or_default().split('.').collect(); + if domain.len() > 1 { + alias.name = format!("{}_{username}", domain[domain.len() - 2]); + } else { + alias.name = format!("{username}"); + } } } Err(_err) => {} @@ -193,25 +208,11 @@ fn csv_assign_config_input_column(config_input: &mut ConfigInputAlias, input_typ match header { FIELD_URL => { let url = Url::parse(value.trim()).map_err(to_io_error)?; - match input_type { - InputType::Xtream => { - let username = url.query_pairs().find(|(key, _)| key == "username").map(|(_, value)| value); - if let Some(uname) = username { - config_input.username = Some(uname.to_string()); - } - let password = url.query_pairs().find(|(key, _)| key == "password").map(|(_, value)| value); - if let Some(pwd) = password { - config_input.password = Some(pwd.to_string()); - } - config_input.url = url.origin().ascii_serialization() - } - - InputType::M3u => { + InputType::Xtream | InputType::M3u => { config_input.url = url.to_string(); } - - InputType::M3uBatch => { + InputType::M3uBatch | InputType::XtreamBatch => { } }; }, @@ -231,7 +232,8 @@ fn csv_assign_config_input_column(config_input: &mut ConfigInputAlias, input_typ Ok(()) } -pub fn csv_read_inputs(input_type: InputType, file_path: &str) -> Result, io::Error> { +pub fn csv_read_inputs(input_type: InputType, file_uri: &str) -> Result, io::Error> { + let file_path = file_uri.strip_prefix("file://").unwrap_or(file_uri); let inputs_file = std::path::PathBuf::from(file_path); if let Ok(file) = file_utils::open_file(&inputs_file) { let mut result = vec![];