From 67aa066b45aecda5d5156d7617b9320b49cd2e46 Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 14 Apr 2025 18:19:01 +0200 Subject: [PATCH] Input Fetch Method added --- CHANGELOG.md | 1 + src/api/api_utils.rs | 4 +-- src/api/model/active_provider_manager.rs | 3 +- src/model/config.rs | 37 ++++++++++++++++++++++++ src/utils/network/request.rs | 32 ++++++++++++++++---- 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 691ad1944..28958433f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ url: ['http://localhost:3001/xmltv.php?epg_id=1', 'http://localhost:3001/xmltv.p - added `estrip` to input for auto epg matching, if not given `["3840p", "uhd", "fhd", "hd", "sd", "4k", "plus", "raw"]` is default When no matching epg_id is found, the display name is used to match a channel name. The given strings are stripped to get a better match. - fixed chno assignment issue +- Added `method` attribute to input config. It can be set to `GET` or `POST`. # 2.2.5 (2025-03-27) - fixed web ui playlist regexp search diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 08a2e8ac7..834ea4917 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -11,7 +11,7 @@ use crate::api::model::streams::provider_stream_factory::BufferStreamOptions; use crate::api::model::streams::shared_stream_manager::SharedStreamManager; use crate::auth::authenticator::Claims; use crate::model::api_proxy::ProxyUserCredentials; -use crate::model::config::{ConfigInput, ConfigTarget}; +use crate::model::config::{ConfigInput, ConfigTarget, InputFetchMethod}; use crate::model::playlist::PlaylistItemType; use crate::tools::lru_cache::LRUResourceCache; use crate::utils::file::file_utils::create_new_file_for_write; @@ -441,7 +441,7 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req_hea } trace_if_enabled!("Try to fetch resource {}", sanitize_sensitive_info(resource_url)); if let Ok(url) = Url::parse(resource_url) { - let client = request::get_client_request(&app_state.http_client, input.map(|i| &i.headers), &url, Some(&req_headers)); + let client = request::get_client_request(&app_state.http_client, input.map_or(InputFetchMethod::GET, |i| i.method), input.map(|i| &i.headers), &url, Some(&req_headers)); match client.send().await { Ok(response) => { let status = response.status(); diff --git a/src/api/model/active_provider_manager.rs b/src/api/model/active_provider_manager.rs index 6273a503e..35f2a8a93 100644 --- a/src/api/model/active_provider_manager.rs +++ b/src/api/model/active_provider_manager.rs @@ -490,7 +490,7 @@ mod tests { use super::*; use crate::Arc; use std::thread; - + use crate::model::config::InputFetchMethod; macro_rules! should_available { ($lineup:expr, $provider_id:expr) => { @@ -542,6 +542,7 @@ mod tests { aliases: None, headers: Default::default(), options: None, + method: InputFetchMethod::default(), } } diff --git a/src/model/config.rs b/src/model/config.rs index b652e6016..c07e06b8b 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -651,6 +651,41 @@ impl FromStr for InputType { } } +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Default)] +pub enum InputFetchMethod { + #[default] + GET, + POST, +} + +impl InputFetchMethod { + const GET_METHOD: &'static str = "GET"; + const POST_METHOD: &'static str = "POST"; +} + +impl Display for InputFetchMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::GET => Self::GET_METHOD, + Self::POST => Self::POST_METHOD, + }) + } +} + +impl FromStr for InputFetchMethod { + type Err = M3uFilterError; + + fn from_str(s: &str) -> Result { + if s.eq(Self::GET_METHOD) { + Ok(Self::GET) + } else if s.eq(Self::POST_METHOD) { + Ok(Self::POST) + } else { + create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Unknown Fetch Method: {}", s) + } + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] pub struct ConfigInputOptions { @@ -942,6 +977,8 @@ pub struct ConfigInput { pub priority: i16, #[serde(default)] pub max_connections: u16, + #[serde(default)] + pub method: InputFetchMethod, } impl ConfigInput { diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index 0c73a7735..fab3f66dc 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -18,7 +18,7 @@ use url::Url; use crate::m3u_filter_error::create_m3u_filter_error_result; use crate::m3u_filter_error::{str_to_io_error, M3uFilterError, M3uFilterErrorKind}; -use crate::model::config::ConfigInput; +use crate::model::config::{ConfigInput, InputFetchMethod}; use crate::model::stats::format_elapsed_time; use crate::repository::storage::{get_input_storage_path, short_hash}; use crate::repository::xtream_repository::FILE_EPG; @@ -126,10 +126,22 @@ pub async fn get_input_text_content(client: Arc, input: &Config } pub fn get_client_request(client: &Arc, + method: InputFetchMethod, headers: Option<&HashMap>, url: &Url, custom_headers: Option<&HashMap>>) -> reqwest::RequestBuilder { - let request = client.get(url.clone()); + let request = match method { + InputFetchMethod::GET => client.get(url.clone()), + InputFetchMethod::POST => { + // let base_url = url[..url::Position::BeforePath].to_string() + url.path(); + let mut params = HashMap::new(); + for (key, value) in url.query_pairs() { + params.insert(key.to_string(), value.to_string()); + } + // we could cut the params but we leave them as query and add them as form. + client.post(url.clone()).form(¶ms) + }, + }; let headers = get_request_headers(headers, custom_headers); request.headers(headers) } @@ -187,7 +199,7 @@ pub fn get_local_file_content(file_path: &PathBuf) -> Result { async fn get_remote_content_as_file(client: Arc, input: &ConfigInput, url: &Url, file_path: &Path) -> Result { let start_time = Instant::now(); - let request = get_client_request(&client, Some(&input.headers), url, None); + let request = get_client_request(&client, input.method, Some(&input.headers), url, None); match request.send().await { Ok(response) => { if response.status().is_success() { @@ -220,7 +232,7 @@ async fn get_remote_content_as_file(client: Arc, input: &Config async fn get_remote_content(client: Arc, input: &ConfigInput, url: &Url) -> Result { let start_time = Instant::now(); - let request = get_client_request(&client, Some(&input.headers), url, None); + let request = get_client_request(&client, input.method, Some(&input.headers), url, None); match request.send().await { Ok(response) => { let is_success = response.status().is_success(); @@ -259,12 +271,16 @@ async fn get_remote_content(client: Arc, input: &ConfigInput, u } if decode_buffer.is_empty() { - match String::from_utf8(bytes.to_vec()) { + let content_bytes = bytes.to_vec(); + match String::from_utf8(content_bytes) { Ok(decoded_content) => { debug_if_enabled!("Request took:{} {}", format_elapsed_time(start_time.elapsed().as_secs()), sanitize_sensitive_info(url.as_str())); Ok(decoded_content) } - Err(err) => Err(str_to_io_error(&format!("failed to plain text content {err}"))) + Err(err) => { + println!("{err:?}"); + Err(str_to_io_error(&format!("failed to plain text content {err}"))) + } } } else { debug_if_enabled!("Request took:{}, {}", format_elapsed_time(start_time.elapsed().as_secs()), sanitize_sensitive_info(url.as_str())); @@ -499,6 +515,10 @@ pub fn get_credentials_from_url_str(url_with_credentials: &str) -> (Option