diff --git a/CHANGELOG.md b/CHANGELOG.md index f99bd5747..a0132375f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,8 @@ - Shared stream shutdown now drops registry locks before releasing provider handles to prevent cross-lock stalls. - Added `order: none` support for group/channel sorting so mappings can opt out of any reordering and keep the source order. - Session tracking now matches repeated HLS segment connections by session token so a single user keeps one active connection count even when new TCP sockets are opened. -- EPG icon urls are now rewritten on reverse proxy mode. +- EPG icon urls are now rewritten on reverse proxy mode. +- Xtream Codes Batch provider accounts are now checked for expiration. # 3.2.0 (2025-11-14) - Added `name` attribute to Staged Input. diff --git a/Cargo.lock b/Cargo.lock index b5808543a..3ad5c8441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1096,7 +1096,7 @@ dependencies = [ [[package]] name = "frontend" -version = "3.2.9" +version = "3.2.10" dependencies = [ "anyhow", "base64", @@ -3765,7 +3765,7 @@ dependencies = [ [[package]] name = "shared" -version = "3.2.9" +version = "3.2.10" dependencies = [ "base64", "bitflags 2.10.0", @@ -4314,7 +4314,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tuliprox" -version = "3.2.9" +version = "3.2.10" dependencies = [ "arc-swap", "async-compression", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 28f84ce15..39a1f4f12 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuliprox" -version = "3.2.9" +version = "3.2.10" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/backend/src/api/endpoints/api_playlist_utils.rs b/backend/src/api/endpoints/api_playlist_utils.rs index 9dfc8c5d1..926046a69 100644 --- a/backend/src/api/endpoints/api_playlist_utils.rs +++ b/backend/src/api/endpoints/api_playlist_utils.rs @@ -141,13 +141,13 @@ pub(in crate::api::endpoints) async fn get_playlist_for_target(cfg_target: Optio (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response() } -pub(in crate::api::endpoints) async fn get_playlist(client: Arc, cfg_input: Option<&ConfigInput>, cfg: &Config, accept: Option<&String>) -> impl IntoResponse + Send { +pub(in crate::api::endpoints) async fn get_playlist(client: Arc, cfg_input: Option<&Arc>, cfg: &Arc, accept: Option<&String>) -> impl IntoResponse + Send { match cfg_input { Some(input) => { let (result, errors) = match input.input_type { InputType::M3u | InputType::M3uBatch => m3u::get_m3u_playlist(client, cfg, input, &cfg.working_dir).await, - InputType::Xtream | InputType::XtreamBatch => xtream::get_xtream_playlist(cfg, client, input, &cfg.working_dir).await, + InputType::Xtream | InputType::XtreamBatch => xtream::get_xtream_playlist(cfg, &client, input, &cfg.working_dir).await, }; if result.is_empty() { let error_strings: Vec = errors.iter().map(std::string::ToString::to_string).collect(); diff --git a/backend/src/api/endpoints/v1_api_playlist.rs b/backend/src/api/endpoints/v1_api_playlist.rs index 4a197f1c4..8ebbff432 100644 --- a/backend/src/api/endpoints/v1_api_playlist.rs +++ b/backend/src/api/endpoints/v1_api_playlist.rs @@ -91,12 +91,12 @@ async fn playlist_content( get_playlist_for_target(app_state.app_config.get_target_by_id(target_id).as_deref(), &app_state.app_config, accept.as_ref()).await.into_response() } PlaylistRequest::Input(input_id) => { - get_playlist(Arc::clone(&app_state.http_client.load()), app_state.app_config.get_input_by_id(input_id).as_deref(), &config, accept.as_ref()).await.into_response() + get_playlist(Arc::clone(&app_state.http_client.load()), app_state.app_config.get_input_by_id(input_id).as_ref(), &config, accept.as_ref()).await.into_response() } PlaylistRequest::CustomXtream(xtream) => { match Url::parse(&xtream.url) { Ok(parsed) if parsed.scheme() == "http" || parsed.scheme() == "https" => { - let input = create_config_input_for_xtream(&xtream.username, &xtream.password, &xtream.url); + let input = Arc::new(create_config_input_for_xtream(&xtream.username, &xtream.password, &xtream.url)); get_playlist(Arc::clone(&app_state.http_client.load()), Some(&input), &config, accept.as_ref()).await.into_response() } _ => { @@ -107,7 +107,7 @@ async fn playlist_content( PlaylistRequest::CustomM3u(m3u) => { match Url::parse(&m3u.url) { Ok(parsed) if parsed.scheme() == "http" || parsed.scheme() == "https" => { - let input = create_config_input_for_m3u(&m3u.url); + let input = Arc::new(create_config_input_for_m3u(&m3u.url)); get_playlist(Arc::clone(&app_state.http_client.load()), Some(&input), &config, accept.as_ref()).await.into_response() } _ => { diff --git a/backend/src/model/config/input.rs b/backend/src/model/config/input.rs index 4524b66c9..ff6b8607f 100644 --- a/backend/src/model/config/input.rs +++ b/backend/src/model/config/input.rs @@ -92,7 +92,7 @@ impl From<&StagedInputDto> for StagedInput { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ConfigInputAlias { pub id: u16, pub name: String, diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index 8db849a2d..73ce77cf7 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -280,11 +280,11 @@ fn is_target_enabled(target: &ConfigTarget, user_targets: &ProcessTargets) -> bo (!user_targets.enabled && target.enabled) || (user_targets.enabled && user_targets.has_target(target.id)) } -async fn playlist_download_from_input(client: &Arc, config: &Arc, input: &ConfigInput) -> (Vec, Vec) { +async fn playlist_download_from_input(client: &Arc, config: &Arc, input: &Arc) -> (Vec, Vec) { let working_dir = &config.working_dir; match input.input_type { InputType::M3u => m3u::get_m3u_playlist(Arc::clone(client), config, input, working_dir).await, - InputType::Xtream => xtream::get_xtream_playlist(config, Arc::clone(client), input, working_dir).await, + InputType::Xtream => xtream::get_xtream_playlist(config, client, input, working_dir).await, InputType::M3uBatch | InputType::XtreamBatch => (vec![], vec![]) } } diff --git a/backend/src/utils/network/xtream.rs b/backend/src/utils/network/xtream.rs index dc9134c1b..12869164f 100644 --- a/backend/src/utils/network/xtream.rs +++ b/backend/src/utils/network/xtream.rs @@ -1,21 +1,21 @@ -use crate::model::{InputSource, ProxyUserCredentials}; +use crate::api::model::AppState; +use crate::messaging::send_message; use crate::model::{Config, ConfigInput, ConfigTarget}; +use crate::model::{InputSource, ProxyUserCredentials}; use crate::processing::parser::xtream; use crate::repository::xtream_repository; use crate::repository::xtream_repository::{rewrite_xtream_series_info_content, rewrite_xtream_vod_info_content, xtream_get_input_info}; +use crate::utils::request; +use chrono::DateTime; +use log::{error, info, warn}; use shared::error::{str_to_io_error, TuliproxError}; -use crate::utils::{request}; -use chrono::{DateTime}; -use log::{info, warn}; +use shared::model::{MsgKind, PlaylistEntry, PlaylistGroup, ProxyUserStatus, XtreamCluster, XtreamPlaylistItem}; +use shared::utils::{extract_extension_from_url, get_i64_from_serde_value, get_string_from_serde_value}; // use std::cmp::Ordering; use std::io::Error; use std::str::FromStr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use shared::model::{MsgKind, PlaylistEntry, PlaylistGroup, ProxyUserStatus, XtreamCluster, XtreamPlaylistItem}; -use shared::utils::{extract_extension_from_url, get_i64_from_serde_value, get_string_from_serde_value}; -use crate::api::model::AppState; -use crate::messaging::{send_message}; #[inline] pub fn get_xtream_stream_url_base(url: &str, username: &str, password: &str) -> String { @@ -191,10 +191,10 @@ async fn xtream_login(cfg: &Config, client: &Arc, input: &Input Ok(()) } -pub async fn get_xtream_playlist(cfg: &Config, client: Arc, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) { +pub async fn get_xtream_playlist(cfg: &Arc, client: &Arc, input: &Arc, working_dir: &str) -> (Vec, Vec) { let input_source: InputSource = { match input.staged.as_ref() { - None => input.into(), + None => input.as_ref().into(), Some(staged) => staged.into(), } }; @@ -204,7 +204,9 @@ pub async fn get_xtream_playlist(cfg: &Config, client: Arc, inp let base_url = get_xtream_stream_url_base(&input_source.url, username, password); let input_source_login = input_source.with_url(base_url.clone()); + check_alias_user_state(cfg, &client, input); if let Err(err) = xtream_login(cfg, &client, &input_source_login, username).await { + error!("Could not log in with xtream user {username} for provider {}. {err}", input.name); return (Vec::with_capacity(0), vec![err]); } @@ -250,6 +252,46 @@ pub async fn get_xtream_playlist(cfg: &Config, client: Arc, inp (playlist_groups, errors) } +fn check_alias_user_state(cfg: &Arc, client: &Arc, input: &Arc) { + let cfg = Arc::clone(cfg); + let client = Arc::clone(client); + let input = Arc::clone(input); + tokio::spawn(async move { + if let Some(aliases) = input.aliases.as_ref() { + for alias in aliases { + // Random wait time 5–20 seconds to avoid provider block + let delay = fastrand::i32(5..=20) as u64; + tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await; + + if let (Some(username), Some(password)) = + (alias.username.as_ref(), alias.password.as_ref()) + { + let mut input_source: InputSource = input.as_ref().into(); + input_source.username = alias.username.clone(); + input_source.password = alias.password.clone(); + input_source.url = alias.url.clone(); + let base_url = get_xtream_stream_url_base( + &input_source.url, + username, + password, + ); + let input_source_login = input_source.with_url(base_url.clone()); + + if let Err(err) = + xtream_login(&cfg, &client, &input_source_login, username).await + { + error!( + "Could not log in with xtream user {} for provider {}. {err}", + username, + alias.name + ); + } + } + } + } + }); +} + pub fn create_vod_info_from_item(target: &ConfigTarget, user: &ProxyUserCredentials, pli: &XtreamPlaylistItem, last_updated: i64) -> String { let category_id = pli.category_id; let stream_id = if user.proxy.is_redirect(pli.item_type) || target.is_force_redirect(pli.item_type) { pli.provider_id } else { pli.virtual_id }; diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index cd8b0acc3..04d3b4292 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "frontend" -version = "3.2.9" +version = "3.2.10" edition = "2021" [dependencies] -shared = { version = "3.2.9", path = "../shared" } +shared = { version = "3.2.10", path = "../shared" } chrono = "0" yew = "0.21" yew-router = "0.18" diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 13c83d580..51385ebaa 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shared" -version = "3.2.9" +version = "3.2.10" edition = "2021" [dependencies]