diff --git a/CHANGELOG.md b/CHANGELOG.md index c4d294c72..333229e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,21 @@ # Changelog +# 2.2.2 (2025-02-xx) +- input alias definition for same provider with same content but different credentials +```yaml +- sources: +- inputs: + - url: 'http://provider.net' + name: my_provider + username: xyz + password: secret1 + aliases: + - url: 'http://provider.net' + username: abcd + password: secret2 + targets: + - name: test +``` + # 2.2.1 (2025-02-14) - Added more info to `/status`. - Refactored unavailable channel replacement streaming. diff --git a/README.md b/README.md index b513bffd7..f253e8670 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ - Schedule updates in server mode. - Running as a CLI tool to deliver playlists through web servers (e.g., Nginx, Apache). - Define multiple filtering targets to create several playlists from a large one. -- Using regular expressions for matching and defining templates for reusability. - DRY (Don't Repeat Yourself): Define templates and reuse them. +- Using regular expressions for matching and defining templates for reusability. - Define filters with statements, e.g.: filter: `(Group ~ "^FR.*") AND NOT (Group ~ ".*XXX.*" OR Group ~ ".*SERIES.*" OR Group ~ ".*MOVIES.*")` -- Sending alerts via Telegram bot or REST when issues arise. +- Sending alerts via Telegram bot, Pushover or REST when issues arise. - Monitoring group changes and sending notifications. +- Sharing live tv connections +- Display own video stream when channel is unavailable If you need to exclude certain entries from a playlist, you can create filters using headers and apply regex-based renaming or mapping. Run `m3u-filter` as a CLI or in server mode for a web-based UI to manage playlist content and save filtered groups. diff --git a/src/api/endpoints/m3u_api.rs b/src/api/endpoints/m3u_api.rs index c4b0753b7..adc722160 100644 --- a/src/api/endpoints/m3u_api.rs +++ b/src/api/endpoints/m3u_api.rs @@ -88,6 +88,7 @@ async fn m3u_api_stream( if user.proxy == ProxyType::Redirect { let redirect_url = if is_hls_request { &replace_extension(&m3u_item.url, "m3u8") } else { &m3u_item.url }; + // TODO alias processing debug_if_enabled!("Redirecting m3u stream request to {}", sanitize_sensitive_info(redirect_url)); return HttpResponse::Found().insert_header(("Location", redirect_url.as_str())).finish(); } diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index 7b77592b4..cdc1da68b 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -175,8 +175,10 @@ impl ProxyUserCredentials { } } if let Some(status) = &self.status { - debug!("User access denied, status invalid: {}", self.username); - return matches!(status, ProxyUserStatus::Active | ProxyUserStatus::Trial); + if !matches!(status, ProxyUserStatus::Active | ProxyUserStatus::Trial) { + debug!("User access denied, status invalid: {status} for user: {}", self.username); + return false; + } } } true diff --git a/src/model/config.rs b/src/model/config.rs index 2d7978acb..99ebc9360 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -30,6 +30,8 @@ use crate::utils::file::file_utils::file_reader; use crate::utils::size_utils::parse_size_base_2; use crate::utils::sys_utils::exit; +const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (AppleTV; U; CPU OS 14_2 like Mac OS X; en-us) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15"; + pub const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[ "name", "title", "group", "id", "chno", "logo", "logo_small", "parent_code", "audio_track", @@ -49,6 +51,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::utils::string_utils::get_trimmed_string; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)] pub enum TargetType { @@ -535,6 +538,46 @@ pub struct InputUserInfo { pub password: String, } +macro_rules! check_input_credentials { + ($this:ident, $input_type:expr) => { + match $input_type { + InputType::M3u => { + if $this.username.is_some() || $this.password.is_some() { + debug!("for input type m3u: username and password are ignored"); + } + } + InputType::Xtream => { + if $this.username.is_none() || $this.password.is_none() { + return Err(info_err!("for input type xtream: username and password are mandatory".to_string())); + } + } + } + }; +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct ConfigInputAlias { + pub url: String, + pub username: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub password: Option, +} + + +impl ConfigInputAlias { + pub fn prepare(&mut self, input_type: &InputType) -> 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())); + } + self.username = get_trimmed_string(&self.username); + self.password = get_trimmed_string(&self.password); + check_input_credentials!(self, input_type); + Ok(()) + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct ConfigInput { @@ -562,45 +605,29 @@ pub struct ConfigInput { pub enabled: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub options: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub aliases: Option>, } impl ConfigInput { pub fn prepare(&mut self, id: u16) -> Result<(), M3uFilterError> { self.id = id; - if self.name.trim().is_empty() { + self.name = self.name.trim().to_string(); + if self.name.is_empty() { return Err(info_err!("name for input is mandatory".to_string())); } - if self.url.trim().is_empty() { + self.url = self.url.trim().to_string(); + if self.url.is_empty() { return Err(info_err!("url for input is mandatory".to_string())); } - if let Some(user_name) = &self.username { - if user_name.trim().is_empty() { - self.username = None; - } + 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().map(|a| a.prepare(input_type))); } - if let Some(password) = &self.password { - if password.trim().is_empty() { - self.password = None; - } - } - match self.input_type { - InputType::M3u => { - if self.username.is_some() || self.password.is_some() { - debug!("for input type m3u: username and password are ignored"); - } - } - InputType::Xtream => { - if self.username.is_none() || self.password.is_none() { - return Err(info_err!("for input type xtream: username and password are mandatory".to_string())); - } - } - } - if let Some(persist_path) = &self.persist { - if persist_path.trim().is_empty() { - self.persist = None; - } - } - Ok(()) } @@ -749,8 +776,7 @@ impl VideoConfig { Some(downl) => { if downl.headers.is_empty() { downl.headers.borrow_mut().insert("Accept".to_string(), "video/*".to_string()); - downl.headers.borrow_mut().insert("User-Agent".to_string(), "Mozilla/5.0 (AppleTV; U; CPU OS 14_2 like Mac OS X; en-us) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15 -".to_string()); + downl.headers.borrow_mut().insert("User-Agent".to_string(), DEFAULT_USER_AGENT.to_string()); } if let Some(episode_pattern) = &downl.episode_pattern { diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 31e382138..1fbd8ea35 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -1,7 +1,7 @@ extern crate unidecode; use crate::Config; -use crate::model::config::ConfigRename; +use crate::model::config::{ConfigInput, ConfigRename}; use crate::utils::network::epg; use crate::utils::network::m3u; use crate::utils::network::xtream; @@ -292,7 +292,9 @@ fn map_playlist_counter(target: &ConfigTarget, playlist: &[PlaylistGroup]) { // If no input is enabled but the user set the target as command line argument, // we force the input to be enabled. // If there are enabled input, then only these are used. -fn is_input_enabled(enabled_inputs: usize, input_enabled: bool, input_id: u16, user_targets: &ProcessTargets) -> bool { +fn is_input_enabled(enabled_inputs: usize, input: &ConfigInput, user_targets: &ProcessTargets) -> bool { + let input_enabled = input.enabled; + let input_id = input.id; if enabled_inputs == 0 { return user_targets.enabled && user_targets.has_input(input_id); } @@ -309,10 +311,10 @@ async fn process_source(client: Arc, cfg: Arc, source_i let mut input_stats = HashMap::::new(); let mut target_stats = Vec::::new(); let mut source_playlists = Vec::with_capacity(128); - let enabled_inputs = source.inputs.iter().filter(|item| item.enabled).count(); - // Downlod the sources + let enabled_inputs = source.inputs.iter().filter(|&input| input.enabled).count(); + // Download the sources for input in &source.inputs { - if is_input_enabled(enabled_inputs, input.enabled, input.id, &user_targets) { + if is_input_enabled(enabled_inputs, input, &user_targets) { let start_time = Instant::now(); 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, diff --git a/src/utils/string_utils.rs b/src/utils/string_utils.rs index b8a8d87e5..c6efab3b8 100644 --- a/src/utils/string_utils.rs +++ b/src/utils/string_utils.rs @@ -30,4 +30,14 @@ impl Capitalize for String { fn capitalize(&self) -> String { self.as_str().capitalize() // Reuse the &str implementation } +} + +pub fn get_trimmed_string(value: &Option) -> Option { + if let Some(v) = value { + let trimmed = v.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + None } \ No newline at end of file