mirror of
https://github.com/euzu/tuliprox.git
synced 2026-09-28 20:12:12 +02:00
264 lines
9.8 KiB
Rust
264 lines
9.8 KiB
Rust
use shared::error::{str_to_io_error, to_io_error};
|
|
use crate::utils::EnvResolvingReader;
|
|
use crate::utils::{file_reader, resolve_relative_path};
|
|
use log::error;
|
|
use std::io;
|
|
use std::io::{BufRead, Cursor, Error};
|
|
use std::path::PathBuf;
|
|
use url::Url;
|
|
use shared::model::{ConfigInputAliasDto, InputType};
|
|
use shared::utils::{get_credentials_from_url, parse_timestamp, trim_last_slash};
|
|
use crate::utils::request::get_local_file_content;
|
|
|
|
const CSV_SEPARATOR: char = ';';
|
|
const HEADER_PREFIX: char = '#';
|
|
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_EXP_DATE: &str = "exp_date";
|
|
const FIELD_UNKNOWN: &str = "?";
|
|
const DEFAULT_COLUMNS: &[&str] = &[FIELD_URL, FIELD_MAX_CON, FIELD_PRIO, FIELD_NAME, FIELD_USERNAME, FIELD_PASSWORD, FIELD_EXP_DATE];
|
|
|
|
fn csv_assign_mandatory_fields(alias: &mut ConfigInputAliasDto, input_type: InputType) {
|
|
if !alias.url.is_empty() {
|
|
match Url::parse(alias.url.as_str()) {
|
|
Ok(url) => {
|
|
let (username, password) = get_credentials_from_url(&url);
|
|
if username.is_none() || password.is_none() {
|
|
// xtream url
|
|
if input_type == InputType::XtreamBatch {
|
|
alias.url = url.origin().ascii_serialization();
|
|
} else if input_type == InputType::M3uBatch && alias.username.is_some() && alias.password.is_some() {
|
|
alias.url = format!("{}/get.php?username={}&password={}&type=m3u_plus",
|
|
trim_last_slash(&url.origin().ascii_serialization()),
|
|
alias.username.as_deref().unwrap_or(""),
|
|
alias.password.as_deref().unwrap_or("")
|
|
);
|
|
}
|
|
} else {
|
|
if input_type == InputType::XtreamBatch {
|
|
alias.url = url.origin().ascii_serialization();
|
|
}
|
|
// m3u url
|
|
alias.username = username;
|
|
alias.password = password;
|
|
}
|
|
|
|
if alias.name.is_empty() {
|
|
let username = alias.username.as_deref().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 = username.to_string();
|
|
}
|
|
}
|
|
}
|
|
Err(_err) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn csv_assign_config_input_column(config_input: &mut ConfigInputAliasDto, header: &str, raw_value: &str) -> Result<(), io::Error> {
|
|
let value = raw_value.trim();
|
|
if !value.is_empty() {
|
|
match header {
|
|
FIELD_URL => {
|
|
let url = Url::parse(value.trim()).map_err(to_io_error)?;
|
|
config_input.url = url.to_string();
|
|
}
|
|
FIELD_MAX_CON => {
|
|
let max_connections = value.parse::<u16>().unwrap_or(1);
|
|
config_input.max_connections = max_connections;
|
|
}
|
|
FIELD_PRIO => {
|
|
let priority = value.parse::<i16>().unwrap_or(0);
|
|
config_input.priority = priority;
|
|
}
|
|
FIELD_NAME => {
|
|
config_input.name = value.to_string();
|
|
}
|
|
FIELD_USERNAME => {
|
|
config_input.username = Some(value.to_string());
|
|
}
|
|
FIELD_PASSWORD => {
|
|
config_input.password = Some(value.to_string());
|
|
}
|
|
FIELD_EXP_DATE => {
|
|
config_input.exp_date = parse_timestamp(value).unwrap_or_else(|e| {
|
|
error!("Failed to parse exp_date '{value}': {e}");
|
|
None
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn csv_read_inputs_from_reader(batch_input_type: InputType, reader: impl BufRead) -> Result<Vec<ConfigInputAliasDto>, Error> {
|
|
let input_type = match batch_input_type {
|
|
InputType::M3uBatch | InputType::M3u => InputType::M3uBatch,
|
|
InputType::XtreamBatch | InputType::Xtream => InputType::XtreamBatch
|
|
};
|
|
let mut result = vec![];
|
|
let mut default_columns = vec![];
|
|
default_columns.extend_from_slice(DEFAULT_COLUMNS);
|
|
let mut header_defined = false;
|
|
for line in reader.lines() {
|
|
let line = line?;
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
if line.starts_with(HEADER_PREFIX) {
|
|
if !header_defined {
|
|
header_defined = true;
|
|
default_columns = line[1..].split(CSV_SEPARATOR).map(|s| {
|
|
match s {
|
|
FIELD_URL => FIELD_URL,
|
|
FIELD_MAX_CON => FIELD_MAX_CON,
|
|
FIELD_PRIO => FIELD_PRIO,
|
|
FIELD_NAME => FIELD_NAME,
|
|
FIELD_USERNAME => FIELD_USERNAME,
|
|
FIELD_PASSWORD => FIELD_PASSWORD,
|
|
FIELD_EXP_DATE => FIELD_EXP_DATE,
|
|
_ => {
|
|
error!("Field {s} is unsupported for csv input");
|
|
FIELD_UNKNOWN
|
|
}
|
|
}
|
|
}).collect();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let mut config_input = ConfigInputAliasDto {
|
|
id: 0,
|
|
name: String::new(),
|
|
url: String::new(),
|
|
username: None,
|
|
password: None,
|
|
priority: 0,
|
|
max_connections: 1,
|
|
exp_date: None,
|
|
};
|
|
|
|
let columns: Vec<&str> = line.split(CSV_SEPARATOR).collect();
|
|
for (&header, &value) in default_columns.iter().zip(columns.iter()) {
|
|
if let Err(err) = csv_assign_config_input_column(&mut config_input, header, value) {
|
|
error!("Could not parse input line: {line} err: {err}");
|
|
}
|
|
}
|
|
csv_assign_mandatory_fields(&mut config_input, input_type);
|
|
result.push(config_input);
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
|
|
pub async fn csv_read_inputs(input_type: InputType, file_uri: &str) -> Result<(PathBuf, Vec<ConfigInputAliasDto>), Error> {
|
|
let file_path = get_csv_file_path(file_uri)?;
|
|
match get_local_file_content(&file_path).await {
|
|
Ok(content) => {
|
|
Ok((file_path, csv_read_inputs_from_reader(input_type, EnvResolvingReader::new(file_reader(Cursor::new(content))))?))
|
|
}
|
|
Err(err) => {
|
|
Err(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_csv_file_path(file_uri: &str) -> Result<PathBuf, Error> {
|
|
if let Ok(url) = file_uri.parse::<Url>() {
|
|
if url.scheme() == "file" {
|
|
match url.to_file_path() {
|
|
Ok(path) => Ok(path),
|
|
Err(()) => Err(str_to_io_error(&format!("Could not open {file_uri}"))),
|
|
}
|
|
} else {
|
|
Err(str_to_io_error(&format!("Only file:// is supported {file_uri}")))
|
|
}
|
|
} else {
|
|
resolve_relative_path(file_uri)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::utils::file::csv_input_reader::csv_read_inputs_from_reader;
|
|
use crate::utils::{file_reader, resolve_env_var};
|
|
use std::io::{Cursor};
|
|
use shared::model::InputType;
|
|
|
|
const M3U_BATCH: &str = r"
|
|
#url;name;max_connections;priority
|
|
http://hd.providerline.com:8080/get.php?username=user1&password=user1&type=m3u_plus;input_1
|
|
http://hd.providerline.com/get.php?username=user2&password=user2&type=m3u_plus;input_2;1;2
|
|
http://hd.providerline.com/get.php?username=user3&password=user3&type=m3u_plus;input_3;1;2
|
|
http://hd.providerline.com/get.php?username=user4&password=user4&type=m3u_plus;input_4
|
|
";
|
|
|
|
const XTREAM_BATCH: &str = r"
|
|
#name;username;password;url;max_connections;exp_date
|
|
input_1;de566567;de2345f43g5;http://provider_1.tv:80;1;2028-11-23 13:12:34
|
|
input_2;de566567;de2345f43g5;http://provider_2.tv:8080;1;2028-12-23 13:12:34
|
|
";
|
|
|
|
#[test]
|
|
fn test_read_inputs_xtream_as_m3u() {
|
|
let reader = file_reader(Cursor::new(XTREAM_BATCH));
|
|
let result = csv_read_inputs_from_reader(InputType::M3uBatch, reader);
|
|
assert!(result.is_ok());
|
|
let aliases = result.unwrap();
|
|
assert!(!aliases.is_empty());
|
|
for config in aliases {
|
|
assert!(config.url.contains("username"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_inputs_m3u_as_m3u() {
|
|
let reader = file_reader(Cursor::new(M3U_BATCH));
|
|
let result = csv_read_inputs_from_reader(InputType::M3uBatch, reader);
|
|
assert!(result.is_ok());
|
|
let aliases = result.unwrap();
|
|
assert!(!aliases.is_empty());
|
|
for config in aliases {
|
|
assert!(config.url.contains("username"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_inputs_xtream_as_xtream() {
|
|
let reader = file_reader(Cursor::new(XTREAM_BATCH));
|
|
let result = csv_read_inputs_from_reader(InputType::XtreamBatch, reader);
|
|
assert!(result.is_ok());
|
|
let aliases = result.unwrap();
|
|
assert!(!aliases.is_empty());
|
|
for config in aliases {
|
|
assert!(!config.url.contains("username"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_inputs_m3u_as_xtream() {
|
|
let reader = file_reader(Cursor::new(M3U_BATCH));
|
|
let result = csv_read_inputs_from_reader(InputType::XtreamBatch, reader);
|
|
assert!(result.is_ok());
|
|
let aliases = result.unwrap();
|
|
assert!(!aliases.is_empty());
|
|
for config in aliases {
|
|
assert!(!config.url.contains("username"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve() {
|
|
let resolved = resolve_env_var("${env:HOME}");
|
|
assert_eq!(resolved, std::env::var("HOME").unwrap());
|
|
}
|
|
}
|