diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 7b744e4f3..e89f7ed4d 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -4,36 +4,6 @@ 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 c6037f6bd..e9d0a5e5c 100644 --- a/src/api/xmltv_api.rs +++ b/src/api/xmltv_api.rs @@ -6,7 +6,7 @@ use url::Url; use crate::api::api_utils::{get_user_target, serve_file}; use crate::api::api_model::{AppState, UserApiRequest}; use crate::model::api_proxy::ProxyType; -use crate::model::config::{Config, ConfigTarget, InputType}; +use crate::model::config::{Config, ConfigTarget}; use crate::model::config::TargetType; use crate::repository::m3u_repository::get_m3u_epg_file_path; use crate::repository::xtream_repository::{get_xtream_epg_file_path, get_xtream_storage_path}; @@ -54,7 +54,7 @@ async fn xmltv_api( // 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(inputs) = _app_state.config.get_inputs_for_target(target_name) { 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()); diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index 2cdebab61..7b674dd0a 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -11,9 +11,8 @@ 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::{Config, ConfigInput, ConfigTarget}; use crate::model::config::{TargetType}; use crate::model::playlist::XtreamCluster; use crate::repository::xtream_repository; @@ -25,26 +24,15 @@ 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 api_utils::parse_m3u_url(input.url.as_str()) { - None => None, - Some(m3u_url_info) => Some( - format!("{}/player_api.php?username={}&password={}&action={}", - m3u_url_info.base_url, - m3u_url_info.username, - m3u_url_info.password, - action - )) - } - } - InputType::Xtream => Some( - format!("{}/player_api.php?username={}&password={}&action={}", - input.url.as_str(), - input.username.as_ref().unwrap_or(&"".to_string()).as_str(), - input.password.as_ref().unwrap_or(&"".to_string()).as_str(), - action - )) + if let Some(user_info) = input.get_user_info() { + Some(format!("{}/player_api.php?username={}&password={}&action={}", + &user_info.base_url, + &user_info.username, + &user_info.password, + action + )) + } else { + None } } @@ -60,25 +48,16 @@ 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 api_utils::parse_m3u_url(input.url.as_str()) { - None => None, - Some(m3u_url_info) => Some( - format!("{}/{}{}/{}/{}", - m3u_url_info.base_url, - ctx_path, - m3u_url_info.username, - m3u_url_info.password, - action_path - )) - } - InputType::Xtream => Some(format!("{}/{}{}/{}/{}", - input.url.as_str(), - ctx_path, - input.username.as_ref().unwrap_or(&"".to_string()).as_str(), - input.password.as_ref().unwrap_or(&"".to_string()).as_str(), - action_path + if let Some(user_info) = input.get_user_info() { + Some(format!("{}/{}{}/{}/{}", + &user_info.base_url, + ctx_path, + &user_info.username, + &user_info.password, + action_path )) + } else { + None } } @@ -146,19 +125,17 @@ 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); - if cfg_input.is_some() { - input = cfg_input; - stream_id = format!("{}{}", xtream_id, action_ext); - } - } - - if input.is_none() { - if let Some(m3u_inputs) = _app_state.config.get_input_for_target(target_name, &InputType::M3u) { - input = m3u_inputs.first().cloned(); + if target.is_multi_input() { + 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); + if cfg_input.is_some() { + input = cfg_input; + stream_id = format!("{}{}", xtream_id, action_ext); + } } + } else if let Some(inputs) = _app_state.config.get_inputs_for_target(target_name) { + input = inputs.first().copied(); } if let Some(target_input) = input { @@ -258,15 +235,13 @@ async fn xtream_player_api_timeshift_stream( xtream_player_api_stream(&req, &api_req, &_app_state, "timeshift", &username, &password, &action_path).await } - fn get_xtream_mapped_id_and_input_for_stream_id<'a>(app_state: &'a AppState, target_name: &str, stream_id: i32) -> (i32, Option<&'a ConfigInput>) { - if let Some(inputs) = app_state.config.get_input_for_target(target_name, &InputType::Xtream) { + if let Some(inputs) = app_state.config.get_inputs_for_target(target_name) { if let Ok(Some(mapping)) = xtream_repository::read_xtream_mapping(stream_id as u32, app_state.config.as_ref(), target_name) { if let Some(cfg_input) = inputs.iter().find(|&&inp| inp.id == mapping.input_id).cloned() { return (mapping.stream_id as i32, Some(cfg_input)); } } - return (stream_id, inputs.first().cloned()); } (stream_id, None) } @@ -303,23 +278,25 @@ async fn xtream_get_stream_info(app_state: &AppState, target_name: &str, stream_ } async fn xtream_get_stream_info_response(app_state: &AppState, user: &UserCredentials, - target_name: &str, stream_id: &str, + target: &ConfigTarget, stream_id: &str, cluster: &XtreamCluster) -> HttpResponse { - let xtream_stream_id: i32 = match FromStr::from_str(stream_id) { + let req_stream_id: i32 = match FromStr::from_str(stream_id) { Ok(id) => id, Err(_) => return HttpResponse::BadRequest().finish() }; - 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) { - return HttpResponse::Found().insert_header(("Location", info_url)).finish(); + if user.proxy == ProxyType::Redirect && !target.is_multi_input() { + if let Some(inputs) = app_state.config.get_inputs_for_target(&target.name) { + if let Some(&input) = inputs.first() { + if let Some(info_url) = get_xtream_player_api_info_url(input, cluster, req_stream_id) { + return HttpResponse::Found().insert_header(("Location", info_url)).finish(); + } } } + return HttpResponse::BadRequest().finish(); } - match xtream_get_stream_info(app_state, target_name, xtream_stream_id, cluster).await { + match xtream_get_stream_info(app_state, &target.name, req_stream_id, cluster).await { Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content), Err(_) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{info:[]}"), } @@ -380,12 +357,12 @@ async fn xtream_player_api( match action { "get_series_info" => { - xtream_get_stream_info_response(_app_state, &user, target_name, + xtream_get_stream_info_response(_app_state, &user, target, api_req.series_id.trim(), &XtreamCluster::Series).await } "get_vod_info" => { - xtream_get_stream_info_response(_app_state, &user, target_name, + xtream_get_stream_info_response(_app_state, &user, target, api_req.vod_id.trim(), &XtreamCluster::Video).await } diff --git a/src/model/config.rs b/src/model/config.rs index b69263255..3e607383c 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -5,6 +5,7 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, RwLock}; +use url::Url; use log::{debug, error, warn}; use path_absolutize::*; @@ -275,6 +276,8 @@ pub(crate) struct ConfigTargetOptions { pub xtream_skip_live_direct_source: bool, #[serde(default = "default_as_true")] pub xtream_skip_video_direct_source: bool, + #[serde(default = "default_as_true")] + pub xtream_skip_series_direct_source: bool, #[serde(default = "default_as_false")] pub xtream_resolve_series: bool, #[serde(default = "default_as_two")] @@ -318,6 +321,8 @@ pub(crate) struct ConfigTarget { pub _filter: Option, #[serde(skip_serializing, skip_deserializing)] pub _mapping: Option>, + #[serde(skip_serializing, skip_deserializing)] + _multi_input: bool, } @@ -387,6 +392,11 @@ impl ConfigTarget { Err(err) => Err(err), } } + + pub(crate) fn is_multi_input(&self) -> bool { + self._multi_input + } + pub(crate) fn filter(&self, provider: &ValueProvider) -> bool { let mut processor = MockValueProcessor {}; return self._filter.as_ref().unwrap().filter(provider, &mut processor); @@ -417,23 +427,23 @@ impl ConfigTarget { pub(crate) struct ConfigSource { pub inputs: Vec, pub targets: Vec, - #[serde(skip_serializing, skip_deserializing)] - pub _multi_xtream_input: bool, } 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; + if self.inputs.len() > 1 { + self.targets.iter_mut().for_each(|t| t._multi_input = true); + } Ok(index + (self.inputs.len() as u16)) } - pub(crate) fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option> { + pub(crate) fn get_inputs_for_target(&self, target_name: &str) -> Option> { let mut result = Vec::new(); for target in &self.targets { if target.name.eq(target_name) { for input in &self.inputs { - if input.enabled && input.input_type.eq(input_type) { + if input.enabled { result.push(input); } } @@ -494,6 +504,12 @@ pub(crate) struct ConfigInputOptions { } +pub(crate) struct InputUserInfo { + pub base_url: String, + pub username: String, + pub password: String, +} + fn default_as_type_m3u() -> InputType { InputType::M3u } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -561,6 +577,37 @@ impl ConfigInput { } Ok(()) } + + pub(crate) fn get_user_info(&self) -> Option { + if self.input_type == InputType::Xtream { + if self.username.is_some() || self.password.is_some() { + return Some(InputUserInfo { + base_url: self.url.to_owned(), + username: self.username.as_ref().unwrap().to_owned(), + password: self.password.as_ref().unwrap().to_owned(), + }); + } + } else if let Ok(url) = Url::parse(&self.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(InputUserInfo { + base_url, + username: username.as_ref().unwrap().to_owned(), + password: password.as_ref().unwrap().to_owned(), + }); + } + } + None + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -731,9 +778,9 @@ impl Config { } } - pub(crate) fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option> { + pub(crate) fn get_inputs_for_target(&self, target_name: &str) -> Option> { for source in &self.sources { - if let Some(cfg) = source.get_input_for_target(target_name, input_type) { + if let Some(cfg) = source.get_inputs_for_target(target_name) { return Some(cfg); } } @@ -769,16 +816,16 @@ impl Config { None } - 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 is_multi_input_target(&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 target.is_multi_input(); + // } + // } + // } + // false + // } pub(crate) fn set_mappings(&mut self, mappings: Option) -> Result<(), M3uFilterError> { if let Some(mapping_list) = mappings { diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 3298db58b..c403c379c 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -279,8 +279,8 @@ pub(crate) struct XtreamSeriesInfoEpisodeInfo { pub duration_secs: u32, pub duration: String, pub movie_image: String, - // "video": [], - // "audio": [], + pub video: Value, + pub audio: Value, pub bitrate: u32, pub rating: f64, pub season: u32, diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index ee7c04af3..508348b00 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -14,7 +14,7 @@ use crate::{Config, get_errors_notify_message, model::config, valid_property}; use crate::filter::{get_field_value, MockValueProcessor, set_field_value, ValueProvider}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::messaging::{MsgKind, send_message}; -use crate::model::config::{ConfigSource, ConfigTarget, default_as_default, InputAffix, InputType, ProcessTargets}; +use crate::model::config::{ConfigTarget, default_as_default, InputAffix, InputType, ProcessTargets}; use crate::model::mapping::{Mapping, MappingValueProcessor}; use crate::model::config::{AFFIX_FIELDS, ItemField, ProcessingOrder, SortOrder::{Asc, Desc}, TargetType}; use crate::model::playlist::{FetchedPlaylist, FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader}; @@ -258,7 +258,7 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option id: grp_id, title: Rc::clone(title), channels: vec![channel.clone()], - xtream_cluster: cluster.clone() + xtream_cluster: cluster.clone(), }) } } @@ -350,7 +350,7 @@ async fn process_source(cfg: Arc, source_idx: usize, user_targets: Arc

{} Err(mut err) => err.drain(..).for_each(|e| errors.push(e)) } @@ -418,7 +418,6 @@ fn get_processing_pipe(target: &ConfigTarget) -> ProcessingPipe { } pub(crate) async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], - source: &ConfigSource, target: &ConfigTarget, cfg: &Config, stats: &mut HashMap, errors: &mut Vec) -> Result<(), Vec> { @@ -456,7 +455,7 @@ pub(crate) async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], apply_affixes(&mut new_fetched_playlists); - if source._multi_xtream_input { + if target.is_multi_input() { let mut stream_id_mappings: Vec = Vec::new(); let mut counter: u32 = 0; new_fetched_playlists.iter()