2023-12-02 20:47:26 +01:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2021-12-21 11:06:51 +01:00
|
|
|
use std::fs;
|
2024-11-02 16:44:38 +01:00
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::{BufWriter, Error, ErrorKind, Read, Write};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2024-11-11 14:57:15 +01:00
|
|
|
use std::sync::LazyLock;
|
2024-11-03 14:52:56 +01:00
|
|
|
use std::time::Instant;
|
2024-09-23 16:51:50 +02:00
|
|
|
|
|
|
|
|
use flate2::read::{GzDecoder, ZlibDecoder};
|
2024-11-02 16:44:38 +01:00
|
|
|
use futures::StreamExt;
|
2024-11-05 15:58:50 +01:00
|
|
|
use log::{debug, error, log_enabled, Level};
|
2024-11-08 11:22:58 +01:00
|
|
|
use regex::Regex;
|
2024-05-25 18:55:28 +02:00
|
|
|
use reqwest::header::CONTENT_ENCODING;
|
2024-11-05 15:58:50 +01:00
|
|
|
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
2024-09-10 20:05:29 +02:00
|
|
|
use url::Url;
|
2021-09-29 20:48:32 +02:00
|
|
|
|
2024-09-23 16:51:50 +02:00
|
|
|
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
|
|
|
|
use crate::model::config::ConfigInput;
|
2024-11-03 14:52:56 +01:00
|
|
|
use crate::model::stats::format_elapsed_time;
|
|
|
|
|
use crate::repository::storage::get_input_storage_path;
|
2024-11-02 16:44:38 +01:00
|
|
|
use crate::repository::xtream_repository::FILE_EPG;
|
2024-11-05 15:58:50 +01:00
|
|
|
use crate::utils::compression_utils::{is_deflate, is_gzip, ENCODING_DEFLATE, ENCODING_GZIP};
|
2024-09-23 16:51:50 +02:00
|
|
|
use crate::utils::file_utils::{get_file_path, persist_file};
|
2024-12-09 19:26:39 +01:00
|
|
|
use crate::{create_m3u_filter_error_result, debug_if_enabled};
|
2024-09-23 16:51:50 +02:00
|
|
|
|
2024-11-04 18:46:56 +01:00
|
|
|
pub const fn bytes_to_megabytes(bytes: u64) -> u64 {
|
2024-03-26 01:04:21 +01:00
|
|
|
bytes / 1_048_576
|
2021-09-29 20:48:32 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-04 18:46:56 +01:00
|
|
|
pub async fn get_input_text_content_as_file(input: &ConfigInput, working_dir: &str, url_str: &str, persist_filepath: Option<PathBuf>) -> Result<PathBuf, M3uFilterError> {
|
2024-12-09 19:26:39 +01:00
|
|
|
debug_if_enabled!("getting input text content working_dir: {}, url: {}", working_dir, mask_sensitive_info(url_str));
|
2024-11-02 16:44:38 +01:00
|
|
|
if url_str.parse::<url::Url>().is_ok() {
|
|
|
|
|
match download_text_content_as_file(input, url_str, working_dir, persist_filepath).await {
|
|
|
|
|
Ok(content) => Ok(content),
|
|
|
|
|
Err(e) => {
|
2024-11-08 11:22:58 +01:00
|
|
|
error!("cant download input url: {} => {}", mask_sensitive_info(url_str), mask_sensitive_info(e.to_string().as_str()));
|
2024-11-02 16:44:38 +01:00
|
|
|
create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to download")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let result = match get_file_path(working_dir, Some(PathBuf::from(url_str))) {
|
|
|
|
|
Some(filepath) => {
|
|
|
|
|
if filepath.exists() {
|
|
|
|
|
if let Some(persist_file_value) = persist_filepath {
|
|
|
|
|
let to_file = &persist_file_value;
|
|
|
|
|
match fs::copy(&filepath, to_file) {
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("cant persist to: {} => {}", to_file.to_str().unwrap_or("?"), e);
|
|
|
|
|
return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to persist: {} => {}", to_file.to_str().unwrap_or("?"), e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if filepath.exists() {
|
|
|
|
|
Some(filepath)
|
|
|
|
|
} else {
|
|
|
|
|
return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed: file does not exists {filepath:?}");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => None
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-04 18:46:56 +01:00
|
|
|
result.map_or_else(|| {
|
2024-11-10 14:17:24 +01:00
|
|
|
let msg = format!("cant read input url: {}", mask_sensitive_info(url_str));
|
2024-11-02 16:44:38 +01:00
|
|
|
error!("{}", msg);
|
|
|
|
|
create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "{}", msg)
|
2024-11-04 18:46:56 +01:00
|
|
|
}, Ok)
|
2024-11-02 16:44:38 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2024-12-13 17:51:47 +01:00
|
|
|
pub async fn get_input_text_content(input: &ConfigInput, working_dir: &str, url_str: &str, persist_filepath: Option<PathBuf>) -> Result<String, M3uFilterError> {
|
2024-12-09 19:26:39 +01:00
|
|
|
debug_if_enabled!("getting input text content working_dir: {}, url: {}", working_dir, mask_sensitive_info(url_str));
|
2024-05-10 12:01:48 +02:00
|
|
|
|
2024-05-27 09:51:35 +02:00
|
|
|
if url_str.parse::<url::Url>().is_ok() {
|
|
|
|
|
match download_text_content(input, url_str, persist_filepath).await {
|
2023-10-13 13:59:22 +02:00
|
|
|
Ok(content) => Ok(content),
|
2021-09-29 20:48:32 +02:00
|
|
|
Err(e) => {
|
2024-11-08 11:22:58 +01:00
|
|
|
error!("cant download input url: {} => {}", mask_sensitive_info(url_str), mask_sensitive_info(e.to_string().as_str()));
|
2023-10-13 13:59:22 +02:00
|
|
|
create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to download")
|
2021-09-29 20:48:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-05-10 12:01:48 +02:00
|
|
|
} else {
|
|
|
|
|
let result = match get_file_path(working_dir, Some(PathBuf::from(url_str))) {
|
|
|
|
|
Some(filepath) => {
|
|
|
|
|
if filepath.exists() {
|
|
|
|
|
if let Some(persist_file_value) = persist_filepath {
|
|
|
|
|
let to_file = &persist_file_value;
|
|
|
|
|
match fs::copy(&filepath, to_file) {
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!("cant persist to: {} => {}", to_file.to_str().unwrap_or("?"), e);
|
|
|
|
|
return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to persist: {} => {}", to_file.to_str().unwrap_or("?"), e);
|
2021-10-15 16:59:20 +02:00
|
|
|
}
|
2024-05-10 12:01:48 +02:00
|
|
|
}
|
|
|
|
|
};
|
2024-09-23 16:51:50 +02:00
|
|
|
|
|
|
|
|
match get_local_file_content(&filepath) {
|
|
|
|
|
Ok(content) => Some(content),
|
2024-05-10 12:01:48 +02:00
|
|
|
Err(err) => {
|
2024-09-23 16:51:50 +02:00
|
|
|
return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed : {}", err);
|
2024-05-10 12:01:48 +02:00
|
|
|
}
|
2021-09-29 20:48:32 +02:00
|
|
|
}
|
2024-05-10 12:01:48 +02:00
|
|
|
} else {
|
|
|
|
|
None
|
2021-10-15 16:59:20 +02:00
|
|
|
}
|
2021-09-29 20:48:32 +02:00
|
|
|
}
|
2024-05-10 12:01:48 +02:00
|
|
|
None => None
|
|
|
|
|
};
|
2024-11-04 18:46:56 +01:00
|
|
|
result.map_or_else(|| {
|
2024-11-10 14:17:24 +01:00
|
|
|
let msg = format!("cant read input url: {}", mask_sensitive_info(url_str));
|
2024-05-10 12:01:48 +02:00
|
|
|
error!("{}", msg);
|
|
|
|
|
create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "{}", msg)
|
2024-11-04 18:46:56 +01:00
|
|
|
}, Ok)
|
2021-09-29 20:48:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-08 21:17:57 +01:00
|
|
|
pub fn get_client_request(headers: Option<&HashMap<String, String>>, url: &Url, custom_headers: Option<&HashMap<String, Vec<u8>>>) -> reqwest::RequestBuilder {
|
2024-11-05 15:58:50 +01:00
|
|
|
let request = reqwest::Client::new().get(url.clone());
|
2025-01-08 21:17:57 +01:00
|
|
|
let headers = get_request_headers(headers, custom_headers);
|
2024-11-05 15:58:50 +01:00
|
|
|
request.headers(headers)
|
2023-10-27 19:03:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-01-08 21:17:57 +01:00
|
|
|
pub fn get_request_headers(defined_headers: Option<&HashMap<String, String>>, custom_headers: Option<&HashMap<String, Vec<u8>>>) -> HeaderMap {
|
2023-12-01 21:38:23 +01:00
|
|
|
let mut headers = HeaderMap::new();
|
2024-11-05 15:58:50 +01:00
|
|
|
if let Some(def_headers) = defined_headers {
|
|
|
|
|
for (key, value) in def_headers {
|
|
|
|
|
headers.insert(
|
|
|
|
|
HeaderName::from_bytes(key.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_bytes(value.as_bytes()).unwrap());
|
|
|
|
|
}
|
2023-12-01 21:38:23 +01:00
|
|
|
}
|
|
|
|
|
if let Some(custom) = custom_headers {
|
2023-12-02 20:47:26 +01:00
|
|
|
let header_keys: HashSet<String> = headers.keys().map(|k| k.as_str().to_lowercase()).collect();
|
2023-12-01 21:38:23 +01:00
|
|
|
for (key, value) in custom {
|
2023-12-02 20:47:26 +01:00
|
|
|
let key_lc = key.to_lowercase();
|
2024-12-09 19:26:39 +01:00
|
|
|
if "host" == key_lc || header_keys.contains(key_lc.as_str()) {
|
2025-01-07 18:41:34 +01:00
|
|
|
debug_if_enabled!("Ignoring request header '{}={}'", key_lc, String::from_utf8_lossy(value));
|
2024-12-09 19:26:39 +01:00
|
|
|
} else {
|
2023-12-01 21:38:23 +01:00
|
|
|
headers.insert(
|
|
|
|
|
HeaderName::from_bytes(key.as_bytes()).unwrap(),
|
|
|
|
|
HeaderValue::from_bytes(value).unwrap());
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-06 21:33:48 +01:00
|
|
|
}
|
2024-03-26 16:07:31 +01:00
|
|
|
if log_enabled!(Level::Debug) {
|
2023-12-05 17:54:42 +01:00
|
|
|
let he: HashMap<String, String> = headers.iter().map(|(k, v)| (k.to_string(), String::from_utf8_lossy(v.as_bytes()).to_string())).collect();
|
2025-01-01 10:59:51 +01:00
|
|
|
if !he.is_empty() {
|
|
|
|
|
debug!("Request headers {:?}", he);
|
|
|
|
|
}
|
2023-12-05 17:54:42 +01:00
|
|
|
}
|
2023-11-06 21:33:48 +01:00
|
|
|
headers
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-23 16:51:50 +02:00
|
|
|
fn get_local_file_content(file_path: &PathBuf) -> Result<String, Error> {
|
|
|
|
|
// Check if the file is accessible
|
|
|
|
|
if file_path.exists() && file_path.is_file() {
|
2024-10-30 11:14:55 +01:00
|
|
|
if let Ok(content) = fs::read(file_path) {
|
2024-09-23 16:51:50 +02:00
|
|
|
if content.len() >= 2 && is_gzip(&content[0..2]) {
|
|
|
|
|
let mut decoder = GzDecoder::new(&content[..]);
|
|
|
|
|
let mut decode_buffer = String::new();
|
|
|
|
|
match decoder.read_to_string(&mut decode_buffer) {
|
|
|
|
|
Ok(_) => return Ok(decode_buffer),
|
|
|
|
|
Err(err) => return Err(Error::new(ErrorKind::Other, format!("failed to decode gzip content {err}")))
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return Ok(String::from_utf8_lossy(&content).parse().unwrap());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let file_str = file_path.to_str().unwrap_or("?");
|
|
|
|
|
Err(Error::new(ErrorKind::InvalidData, format!("Cant find file {file_str}")))
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 16:44:38 +01:00
|
|
|
|
|
|
|
|
async fn get_remote_content_as_file(input: &ConfigInput, url: &Url, file_path: &Path) -> Result<PathBuf, std::io::Error> {
|
2024-11-03 14:52:56 +01:00
|
|
|
let start_time = Instant::now();
|
2025-01-08 21:17:57 +01:00
|
|
|
let request = get_client_request(Some(&input.headers), url, None);
|
2024-11-02 16:44:38 +01:00
|
|
|
match request.send().await {
|
|
|
|
|
Ok(response) => {
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
// Open a file in write mode
|
|
|
|
|
let mut file = BufWriter::with_capacity(8192, File::create(file_path)?);
|
|
|
|
|
// Stream the response body in chunks
|
|
|
|
|
let mut stream = response.bytes_stream();
|
|
|
|
|
while let Some(chunk) = stream.next().await {
|
|
|
|
|
match chunk {
|
|
|
|
|
Ok(bytes) => {
|
|
|
|
|
file.write_all(&bytes)?;
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to read chunk: {err}")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file.flush()?;
|
2024-11-03 14:52:56 +01:00
|
|
|
let elapsed = start_time.elapsed().as_secs();
|
|
|
|
|
debug!("File downloaded successfully to {file_path:?}, took:{}", format_elapsed_time(elapsed));
|
2024-11-02 16:44:38 +01:00
|
|
|
Ok(file_path.to_path_buf())
|
|
|
|
|
} else {
|
2025-01-01 16:11:05 +01:00
|
|
|
Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Request failed with status {} {}", response.status(), mask_sensitive_info(url.as_str()))))
|
2024-11-02 16:44:38 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-01-01 16:11:05 +01:00
|
|
|
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Request failed: {} {err}", mask_sensitive_info(url.as_str())))),
|
2024-11-02 16:44:38 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-23 16:51:50 +02:00
|
|
|
async fn get_remote_content(input: &ConfigInput, url: &Url) -> Result<String, Error> {
|
2024-11-03 14:52:56 +01:00
|
|
|
let start_time = Instant::now();
|
2025-01-08 21:17:57 +01:00
|
|
|
let request = get_client_request(Some(&input.headers), url, None);
|
2024-09-23 16:51:50 +02:00
|
|
|
match request.send().await {
|
|
|
|
|
Ok(response) => {
|
|
|
|
|
let is_success = response.status().is_success();
|
|
|
|
|
if is_success {
|
|
|
|
|
let header_value = response.headers().get(CONTENT_ENCODING);
|
2024-11-04 18:46:56 +01:00
|
|
|
let mut encoding = header_value.and_then(|encoding_header| encoding_header.to_str().map_or(None, |value| Some(value.to_string())));
|
2024-09-23 16:51:50 +02:00
|
|
|
match response.bytes().await {
|
|
|
|
|
Ok(bytes) => {
|
2024-11-03 14:52:56 +01:00
|
|
|
if bytes.len() >= 2 {
|
|
|
|
|
if is_gzip(&bytes[0..2]) {
|
|
|
|
|
encoding = Some(ENCODING_GZIP.to_string());
|
|
|
|
|
} else if is_deflate(&bytes[0..2]) {
|
|
|
|
|
encoding = Some(ENCODING_DEFLATE.to_string());
|
|
|
|
|
}
|
2023-10-06 19:24:06 +02:00
|
|
|
}
|
2024-11-03 14:52:56 +01:00
|
|
|
|
2024-09-23 16:51:50 +02:00
|
|
|
let mut decode_buffer = String::new();
|
|
|
|
|
if let Some(encoding_type) = encoding {
|
|
|
|
|
match encoding_type.as_str() {
|
2024-11-01 15:40:45 +01:00
|
|
|
ENCODING_GZIP => {
|
2024-09-23 16:51:50 +02:00
|
|
|
let mut decoder = GzDecoder::new(&bytes[..]);
|
|
|
|
|
match decoder.read_to_string(&mut decode_buffer) {
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(err) => return Err(std::io::Error::new(ErrorKind::Other, format!("failed to decode gzip content {err}")))
|
|
|
|
|
};
|
2024-11-03 14:52:56 +01:00
|
|
|
}
|
2024-11-01 15:40:45 +01:00
|
|
|
ENCODING_DEFLATE => {
|
2024-09-23 16:51:50 +02:00
|
|
|
let mut decoder = ZlibDecoder::new(&bytes[..]);
|
|
|
|
|
match decoder.read_to_string(&mut decode_buffer) {
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(err) => return Err(std::io::Error::new(ErrorKind::Other, format!("failed to decode zlib content {err}")))
|
2024-05-27 09:51:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-23 16:51:50 +02:00
|
|
|
_ => {}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if decode_buffer.is_empty() {
|
|
|
|
|
match String::from_utf8(bytes.to_vec()) {
|
2024-11-03 14:52:56 +01:00
|
|
|
Ok(decoded_content) => {
|
2025-01-01 10:59:51 +01:00
|
|
|
debug_if_enabled!("Request took:{} {}", format_elapsed_time(start_time.elapsed().as_secs()), mask_sensitive_info(url.as_str()));
|
2024-11-03 14:52:56 +01:00
|
|
|
Ok(decoded_content)
|
2024-11-05 15:58:50 +01:00
|
|
|
}
|
2024-09-23 16:51:50 +02:00
|
|
|
Err(err) => Err(std::io::Error::new(ErrorKind::Other, format!("failed to plain text content {err}")))
|
2024-05-25 18:55:28 +02:00
|
|
|
}
|
2024-09-23 16:51:50 +02:00
|
|
|
} else {
|
2025-01-01 10:59:51 +01:00
|
|
|
debug_if_enabled!("Request took:{}, {}", format_elapsed_time(start_time.elapsed().as_secs()), mask_sensitive_info(url.as_str()));
|
2024-09-23 16:51:50 +02:00
|
|
|
Ok(decode_buffer)
|
2023-10-27 19:03:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-01-01 16:11:05 +01:00
|
|
|
Err(err) => Err(std::io::Error::new(ErrorKind::Other, format!("failed to read response {} {err}", mask_sensitive_info(url.as_str()))))
|
2023-10-27 19:03:29 +02:00
|
|
|
}
|
2024-09-23 16:51:50 +02:00
|
|
|
} else {
|
2025-01-01 16:11:05 +01:00
|
|
|
Err(std::io::Error::new(ErrorKind::Other, format!("Request failed with status {} {}", response.status(), mask_sensitive_info(url.as_str()))))
|
2024-09-23 16:51:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-01-01 16:11:05 +01:00
|
|
|
Err(err) => Err(std::io::Error::new(ErrorKind::Other, format!("Request failed {} {err}", mask_sensitive_info(url.as_str()))))
|
2024-09-23 16:51:50 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-04 18:46:56 +01:00
|
|
|
pub async fn download_text_content_as_file(input: &ConfigInput, url_str: &str, working_dir: &str, persist_filepath: Option<PathBuf>) -> Result<PathBuf, Error> {
|
2024-11-02 16:44:38 +01:00
|
|
|
if let Ok(url) = url_str.parse::<url::Url>() {
|
|
|
|
|
if url.scheme() == "file" {
|
2024-11-08 11:22:58 +01:00
|
|
|
url.to_file_path().map_or_else(|()| Err(Error::new(ErrorKind::Unsupported, format!("Unknown file {}", mask_sensitive_info(url_str)))), |file_path| if file_path.exists() {
|
2024-11-05 15:58:50 +01:00
|
|
|
Ok(file_path)
|
|
|
|
|
} else {
|
|
|
|
|
Err(Error::new(ErrorKind::NotFound, format!("Unknown file {file_path:?}")))
|
|
|
|
|
})
|
2024-11-02 16:44:38 +01:00
|
|
|
} else {
|
2024-11-05 15:58:50 +01:00
|
|
|
let file_path = persist_filepath.map_or_else(|| match get_input_storage_path(input, working_dir) {
|
2024-11-04 18:46:56 +01:00
|
|
|
Ok(download_path) => {
|
|
|
|
|
Ok(download_path.join(FILE_EPG))
|
2024-11-02 16:44:38 +01:00
|
|
|
}
|
2024-11-04 18:46:56 +01:00
|
|
|
Err(err) => Err(err)
|
|
|
|
|
}, Ok);
|
2024-11-02 16:44:38 +01:00
|
|
|
match file_path {
|
|
|
|
|
Ok(persist_path) => get_remote_content_as_file(input, &url, &persist_path).await,
|
|
|
|
|
Err(err) => Err(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2024-11-10 14:17:24 +01:00
|
|
|
Err(std::io::Error::new(ErrorKind::Unsupported, format!("Malformed URL {}", mask_sensitive_info(url_str))))
|
2024-11-02 16:44:38 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2024-11-04 18:46:56 +01:00
|
|
|
pub async fn download_text_content(input: &ConfigInput, url_str: &str, persist_filepath: Option<PathBuf>) -> Result<String, Error> {
|
2024-09-23 16:51:50 +02:00
|
|
|
if let Ok(url) = url_str.parse::<url::Url>() {
|
|
|
|
|
let result = if url.scheme() == "file" {
|
2024-11-08 11:22:58 +01:00
|
|
|
url.to_file_path().map_or_else(|()| Err(Error::new(ErrorKind::Other, format!("Unknown file {}", mask_sensitive_info(url_str)))), |file_path| get_local_file_content(&file_path))
|
2024-09-23 16:51:50 +02:00
|
|
|
} else {
|
|
|
|
|
get_remote_content(input, &url).await
|
2024-05-27 09:51:35 +02:00
|
|
|
};
|
|
|
|
|
match result {
|
|
|
|
|
Ok(content) => {
|
|
|
|
|
if persist_filepath.is_some() {
|
|
|
|
|
persist_file(persist_filepath, &content);
|
|
|
|
|
}
|
|
|
|
|
Ok(content)
|
|
|
|
|
}
|
|
|
|
|
Err(err) => Err(err)
|
2023-10-27 19:03:29 +02:00
|
|
|
}
|
2024-05-27 09:51:35 +02:00
|
|
|
} else {
|
2024-11-10 14:17:24 +01:00
|
|
|
Err(std::io::Error::new(ErrorKind::Other, format!("Malformed URL {}", mask_sensitive_info(url_str))))
|
2024-05-27 09:51:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn download_json_content(input: &ConfigInput, url: &str, persist_filepath: Option<PathBuf>) -> Result<serde_json::Value, Error> {
|
2024-12-09 19:26:39 +01:00
|
|
|
debug_if_enabled!("downloading json content from {}", mask_sensitive_info(url));
|
2024-05-27 09:51:35 +02:00
|
|
|
match download_text_content(input, url, persist_filepath).await {
|
2024-05-25 18:55:28 +02:00
|
|
|
Ok(content) => {
|
2024-05-27 09:51:35 +02:00
|
|
|
match serde_json::from_str::<serde_json::Value>(&content) {
|
|
|
|
|
Ok(value) => Ok(value),
|
|
|
|
|
Err(err) => Err(Error::new(ErrorKind::Other, format!("Failed to parse json {err}")))
|
2024-05-25 18:55:28 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => Err(err)
|
2023-10-27 19:03:29 +02:00
|
|
|
}
|
2024-05-27 09:51:35 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-04 18:46:56 +01:00
|
|
|
pub async fn get_input_json_content(input: &ConfigInput, url: &str, persist_filepath: Option<PathBuf>) -> Result<serde_json::Value, M3uFilterError> {
|
2024-05-27 09:51:35 +02:00
|
|
|
match download_json_content(input, url, persist_filepath).await {
|
|
|
|
|
Ok(content) => Ok(content),
|
2024-11-08 11:22:58 +01:00
|
|
|
Err(e) => create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "cant download input url: {} => {}", mask_sensitive_info(url), mask_sensitive_info(e.to_string().as_str()))
|
2024-05-27 09:51:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-10-25 00:39:22 +02:00
|
|
|
//
|
2024-11-04 18:46:56 +01:00
|
|
|
// pub fn get_base_url(url: &str) -> Option<String> {
|
2024-10-25 00:39:22 +02:00
|
|
|
// if let Some((scheme_end, rest)) = url.split_once("://") {
|
|
|
|
|
// let scheme = scheme_end;
|
|
|
|
|
// if let Some(authority_end) = rest.find('/') {
|
|
|
|
|
// let authority = &rest[..authority_end];
|
|
|
|
|
// return Some(format!("{}://{}", scheme, authority));
|
|
|
|
|
// }
|
|
|
|
|
// return Some(format!("{}://{}", scheme, rest));
|
|
|
|
|
// }
|
|
|
|
|
// None
|
|
|
|
|
// }
|
2024-11-08 11:22:58 +01:00
|
|
|
|
2024-11-11 14:57:15 +01:00
|
|
|
static USERNAME_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| Regex::new(r"(username=)[^&]*").unwrap());
|
|
|
|
|
static PASSWORD_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| Regex::new(r"(password=)[^&]*").unwrap());
|
|
|
|
|
static TOKEN_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| Regex::new(r"(token=)[^&]*").unwrap());
|
2025-01-07 17:52:12 +01:00
|
|
|
static STREAM_URL: LazyLock<regex::Regex> = LazyLock::new(|| Regex::new(r"(.*://).*/(live|video|movie|series|m3u-stream)/\w+/\w+").unwrap());
|
2024-11-08 11:22:58 +01:00
|
|
|
|
|
|
|
|
pub fn mask_sensitive_info(query: &str) -> String {
|
|
|
|
|
// Replace with "***"
|
2024-11-11 14:57:15 +01:00
|
|
|
let masked_query = USERNAME_REGEX.replace_all(query, "$1***");
|
|
|
|
|
let masked_query = PASSWORD_REGEX.replace_all(&masked_query, "$1***");
|
|
|
|
|
let masked_query = TOKEN_REGEX.replace_all(&masked_query, "$1***");
|
2025-01-07 17:52:12 +01:00
|
|
|
let masked_query =STREAM_URL.replace_all(&masked_query, "$1***/$2/***");
|
2024-11-08 11:22:58 +01:00
|
|
|
masked_query.to_string()
|
2024-12-31 12:25:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn extract_extension_from_url(url: &str) -> Option<&str> {
|
|
|
|
|
if let Some(protocol_pos) = url.find("://") {
|
|
|
|
|
if let Some(last_slash_pos) = url[protocol_pos + 3..].rfind('/') {
|
|
|
|
|
let path = &url[protocol_pos + 3 + last_slash_pos + 1..];
|
|
|
|
|
if let Some(last_dot_pos) = path.rfind('.') {
|
|
|
|
|
return Some(&path[last_dot_pos..]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-12-31 12:31:40 +01:00
|
|
|
} else if let Some(last_dot_pos) = url.rfind('.') {
|
|
|
|
|
if last_dot_pos > url.rfind('/').unwrap_or(0) {
|
|
|
|
|
return Some(&url[last_dot_pos..]);
|
2024-12-31 12:25:48 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
2025-01-07 17:52:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use crate::utils::request_utils::STREAM_URL;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_url_mask() {
|
|
|
|
|
// Replace with "***"
|
|
|
|
|
let masked_query = "https://bubblegum.tv/live/username/password/2344.ts";
|
|
|
|
|
let masked_query = STREAM_URL.replace_all(&masked_query, "$1***/$2/***");
|
|
|
|
|
println!("{masked_query}")
|
|
|
|
|
}
|
2024-11-08 11:22:58 +01:00
|
|
|
}
|