diff --git a/CHANGELOG.md b/CHANGELOG.md index 0873a92a5..42610976f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog # 2.1.4 (2025-01-xx) - !BREAKING CHANGE! unique `input` `name` is now mandatory, because rearranging the `source.yml` could lead to wrong results without a playlist update. +_- Added new option to `input` `xtream_live_stream_without_extension`. Default is `false`. Some providers don't like `.ts` extension, some providers need it. +Now you can disable or enable it for a provider. # 2.1.3 (2025-01-26) - Hotfix 2.1.2, forgot to update the stream api code. diff --git a/README.md b/README.md index 491521aa5..6764d71bc 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,7 @@ Each input has the following attributes: + `xtream_skip_live` true or false, live section can be skipped. + `xtream_skip_vod` true or false, vod section can be skipped. + `xtream_skip_series` true or false, series section can be skipped. + + `xtream_live_stream_without_extension` default false, if set to true `.ts` extension is not added to he stream link. `persist` should be different for `m3u` and `xtream` types. For `m3u` use full filename like `./playlist_{}.m3u`. @@ -344,6 +345,7 @@ sources: epg_url: 'test-epg.xml' enabled: false persist: 'playlist_1_{}.m3u' + options: {xtream_skip_series: true} - url: 'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/ad.m3u' - url: 'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/au.m3u' - url: 'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/za.m3u' diff --git a/frontend/src/component/checkbox/checkbox.tsx b/frontend/src/component/checkbox/checkbox.tsx index 4fd1c8024..2e092b85d 100644 --- a/frontend/src/component/checkbox/checkbox.tsx +++ b/frontend/src/component/checkbox/checkbox.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useRef} from "react"; +import React, {useCallback, useLayoutEffect, useRef} from "react"; import './checkbox.scss'; interface CheckboxProps { @@ -20,14 +20,21 @@ export default function Checkbox(props: CheckboxProps) { onSelect?.(inputRef.current.checked, value, evt); }, [value, onSelect]); + useLayoutEffect(() => { + if (inputRef.current) { + inputRef.current.checked = checked; + } + }, [checked]); + if (label) { return ; } else { return
- +
} diff --git a/src/api/model/config.rs b/src/api/model/config.rs index c7c078b4c..80f4d3d94 100644 --- a/src/api/model/config.rs +++ b/src/api/model/config.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::model::api_proxy::ApiProxyConfig; -use crate::model::config::{ProcessingOrder, ReverseProxyConfig, ScheduleConfig}; +use crate::model::config::{ProcessingOrder, ReverseProxyConfig, ScheduleConfig, WebAuthConfig}; use crate::model::config::{ConfigApi, ConfigRename, ConfigSort, ConfigTargetOptions, InputType, MessagingConfig, TargetOutput, VideoConfig}; #[derive(Deserialize, Serialize, Debug, Clone)] @@ -50,5 +50,10 @@ pub struct ServerConfig { pub messaging: Option, pub video: Option, pub api_proxy: Option, + pub log_sanitize_sensitive_info: bool, + pub update_on_boot: bool, + pub web_ui_enabled: bool, + pub web_auth: Option, + } diff --git a/src/api/v1_api.rs b/src/api/v1_api.rs index ab2917957..3c68dc4c7 100644 --- a/src/api/v1_api.rs +++ b/src/api/v1_api.rs @@ -16,7 +16,7 @@ use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCrede use crate::model::config::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType}; use crate::processing::playlist_processor; use crate::utils::request_utils::sanitize_sensitive_info; -use crate::utils::{config_reader, download}; +use crate::utils::{config_reader, m3u_utils, xtream_utils}; fn intern_save_config_api_proxy(backup_dir: &str, api_proxy: &ApiProxyConfig, file_path: &str) -> Option { match config_reader::save_api_proxy(file_path, backup_dir, api_proxy) { @@ -124,6 +124,7 @@ fn create_config_input_for_url(name: &str, url: &str) -> ConfigInput { xtream_skip_live: false, xtream_skip_vod: false, xtream_skip_series: false, + xtream_live_stream_without_extension: false, }), ..Default::default() } @@ -134,8 +135,8 @@ async fn get_playlist(client: Arc, cfg_input: Option<&ConfigInp Some(input) => { let (result, errors) = match input.input_type { - InputType::M3u => download::get_m3u_playlist(client, cfg, input, &cfg.working_dir).await, - InputType::Xtream => download::get_xtream_playlist(client, input, &cfg.working_dir).await, + InputType::M3u => m3u_utils::get_m3u_playlist(client, cfg, input, &cfg.working_dir).await, + InputType::Xtream => xtream_utils::get_xtream_playlist(client, input, &cfg.working_dir).await, }; if result.is_empty() { let error_strings: Vec = errors.iter().map(std::string::ToString::to_string).collect(); @@ -200,6 +201,10 @@ async fn config( threads: config.threads, working_dir: config.working_dir.clone(), backup_dir: config.backup_dir.clone(), + log_sanitize_sensitive_info: config.log_sanitize_sensitive_info, + update_on_boot: config.update_on_boot, + web_ui_enabled: config.web_ui_enabled, + web_auth: config.web_auth.clone(), schedules: config.schedules.clone(), reverse_proxy: config.reverse_proxy.clone(), messaging: config.messaging.clone(), diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index e6a501785..4f694b6d4 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -30,7 +30,7 @@ use crate::repository::xtream_repository; use crate::repository::xtream_repository::{TAG_EPISODES, TAG_INFO_DATA, TAG_SEASONS_DATA}; use crate::utils::json_utils::get_u32_from_serde_value; use crate::utils::request_utils::{extract_extension_from_url, sanitize_sensitive_info}; -use crate::utils::{download, json_utils, request_utils}; +use crate::utils::{json_utils, request_utils, xtream_utils}; use crate::{debug_if_enabled, info_err}; const ACTION_GET_SERIES_INFO: &str = "get_series_info"; @@ -463,11 +463,11 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC if let Ok((pli, _)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(cluster)).await { let input_name = Rc::clone(&pli.input_name); if let Some(input) = app_state.config.get_input_by_name(input_name.as_str()) { - if let Some(info_url) = download::get_xtream_player_api_info_url(input, cluster, pli.provider_id) { + if let Some(info_url) = xtream_utils::get_xtream_player_api_info_url(input, cluster, pli.provider_id) { // Redirect is only possible for live streams, vod and series info needs to be modified if user.proxy == ProxyType::Redirect && cluster == XtreamCluster::Live { return HttpResponse::Found().insert_header(("Location", info_url)).finish(); - } else if let Ok(content) = download::get_xtream_stream_info(Arc::clone(&app_state.http_client), &app_state.config, user, input, target, &pli, info_url.as_str(), cluster).await { + } else if let Ok(content) = xtream_utils::get_xtream_stream_info(Arc::clone(&app_state.http_client), &app_state.config, user, input, target, &pli, info_url.as_str(), cluster).await { return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content); } } @@ -491,7 +491,7 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, if let Ok((pli, _)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None).await { let input_name = Rc::clone(&pli.input_name); if let Some(input) = app_state.config.get_input_by_name(input_name.as_str()) { - if let Some(action_url) = download::get_xtream_player_api_action_url(input, ACTION_GET_SHORT_EPG) { + if let Some(action_url) = xtream_utils::get_xtream_player_api_action_url(input, ACTION_GET_SHORT_EPG) { let mut info_url = format!("{action_url}&{TAG_STREAM_ID}={}", pli.provider_id); if !(limit.is_empty() || limit.eq("0")) { info_url = format!("{info_url}&limit={limit}"); @@ -540,8 +540,8 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget let virtual_id: u32 = try_result_bad_request!(FromStr::from_str(stream_id)); let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(XtreamCluster::Live)).await); let input = try_option_bad_request!(app_state.config.get_input_by_name(pli.input_name.as_str())); - let info_url = try_option_bad_request!(download::get_xtream_player_api_action_url(input, ACTION_GET_CATCHUP_TABLE).map(|action_url| format!("{action_url}&{TAG_STREAM_ID}={}&start={start}&end={end}", pli.provider_id))); - let content = try_result_bad_request!(download::get_xtream_stream_info_content(Arc::clone(&app_state.http_client), info_url.as_str(), input).await); + let info_url = try_option_bad_request!(xtream_utils::get_xtream_player_api_action_url(input, ACTION_GET_CATCHUP_TABLE).map(|action_url| format!("{action_url}&{TAG_STREAM_ID}={}&start={start}&end={end}", pli.provider_id))); + let content = try_result_bad_request!(xtream_utils::get_xtream_stream_info_content(Arc::clone(&app_state.http_client), info_url.as_str(), input).await); let mut doc: Map = try_result_bad_request!(serde_json::from_str(&content)); let epg_listings = try_option_bad_request!(doc.get_mut(TAG_EPG_LISTINGS).and_then(Value::as_array_mut)); let target_path = try_option_bad_request!(get_target_storage_path(&app_state.config, target.name.as_str())); diff --git a/src/model/config.rs b/src/model/config.rs index 044fd7842..2ad9041c6 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -555,6 +555,8 @@ pub struct ConfigInputOptions { pub xtream_skip_vod: bool, #[serde(default)] pub xtream_skip_series: bool, + #[serde(default)] + pub xtream_live_stream_without_extension: bool, } pub struct InputUserInfo { diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index 0d2e3738b..64f7a7e8f 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -1,5 +1,8 @@ extern crate unidecode; +use crate::utils::epg_utils; +use crate::utils::m3u_utils; +use crate::utils::xtream_utils; use crate::repository::storage::hash_string; use async_std::sync::Mutex; use core::cmp::Ordering; @@ -30,7 +33,6 @@ use crate::processing::xtream_processor_series::playlist_resolve_series; use crate::processing::xtream_processor_vod::playlist_resolve_vod; use crate::repository::playlist_repository::persist_playlist; use crate::utils::default_utils::default_as_default; -use crate::utils::download; use crate::{debug_if_enabled, get_errors_notify_message, model::config, notify_err, Config}; fn is_valid(pli: &PlaylistItem, target: &ConfigTarget) -> bool { @@ -302,11 +304,11 @@ async fn process_source(client: Arc, cfg: Arc, source_i if is_input_enabled(enabled_inputs, input.enabled, input.id, &user_targets) { let start_time = Instant::now(); let (mut playlistgroups, mut error_list) = match input.input_type { - InputType::M3u => download::get_m3u_playlist(Arc::clone(&client), &cfg, input, &cfg.working_dir).await, - InputType::Xtream => download::get_xtream_playlist(Arc::clone(&client), input, &cfg.working_dir).await, + InputType::M3u => m3u_utils::get_m3u_playlist(Arc::clone(&client), &cfg, input, &cfg.working_dir).await, + InputType::Xtream => xtream_utils::get_xtream_playlist(Arc::clone(&client), input, &cfg.working_dir).await, }; let (tvguide, mut tvguide_errors) = if error_list.is_empty() { - download::get_xmltv(Arc::clone(&client), &cfg, input, &cfg.working_dir).await + epg_utils::get_xmltv(Arc::clone(&client), &cfg, input, &cfg.working_dir).await } else { (None, vec![]) }; diff --git a/src/processing/xtream_parser.rs b/src/processing/xtream_parser.rs index 0de2df820..7644f5e0d 100644 --- a/src/processing/xtream_parser.rs +++ b/src/processing/xtream_parser.rs @@ -10,6 +10,7 @@ use crate::model::config::ConfigInput; use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; use crate::model::xtream::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream}; use crate::repository::storage::hash_string; +use crate::utils::xtream_utils::get_xtream_stream_url_base; fn map_to_xtream_category(categories: &Value) -> Result, M3uFilterError> { match serde_json::from_value::>(categories.to_owned()) { @@ -78,16 +79,17 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s } } -fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str, stream: &XtreamStream) -> Rc { +fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str, + stream: &XtreamStream, live_stream_without_extension: bool) -> Rc { if stream.direct_source.is_empty() { let stream_base_url = match xtream_cluster { - XtreamCluster::Live => format!("{url}/live/{username}/{password}/{}.ts", &stream.get_stream_id()), + XtreamCluster::Live => format!("{url}/live/{username}/{password}/{}{}", &stream.get_stream_id(), if live_stream_without_extension { "" } else { ".ts" }), XtreamCluster::Video => { let ext = stream.container_extension.as_ref().map_or("mp4", |e| e.as_str()); format!("{url}/movie/{username}/{password}/{}.{ext}", &stream.get_stream_id()) } XtreamCluster::Series => - format!("{url}/player_api.php?username={username}&password={password}&action=get_series_info&series_id={}", &stream.get_stream_id()) + format!("{}&action=get_series_info&series_id={}", get_xtream_stream_url_base(url, username, password), &stream.get_stream_id()) }; Rc::new(stream_base_url) } else { @@ -117,11 +119,14 @@ pub fn parse_xtream(input: &ConfigInput, category_name: Rc::new("Unknown".to_string()), channels: vec![], }); + + let live_stream_without_extension = input.options.as_ref().is_some_and(|o| o.xtream_live_stream_without_extension); + for stream in xtream_streams { let group = group_map.get(&stream.category_id).unwrap_or(&unknown_grp); let mut grp = group.borrow_mut(); let category_name = &grp.category_name; - let stream_url = create_xtream_url(xtream_cluster, url, username, password, &stream); + let stream_url = create_xtream_url(xtream_cluster, url, username, password, &stream, live_stream_without_extension); let item = PlaylistItem { header: RefCell::new(PlaylistItemHeader { id: Rc::new(stream.get_stream_id().to_string()), diff --git a/src/processing/xtream_processor.rs b/src/processing/xtream_processor.rs index 0d6b4220e..e2809ee98 100644 --- a/src/processing/xtream_processor.rs +++ b/src/processing/xtream_processor.rs @@ -2,7 +2,7 @@ use crate::m3u_filter_error::{str_to_io_error, to_io_error, M3uFilterError, M3uF use crate::model::config::{Config, ConfigInput}; use crate::model::playlist::{FetchedPlaylist, PlaylistEntry, PlaylistItem, PlaylistItemType, XtreamCluster}; use crate::repository::storage::get_input_storage_path; -use crate::utils::download; +use crate::utils::{xtream_utils}; use crate::{info_err, notify_err}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -57,8 +57,8 @@ macro_rules! create_resolve_options_function_for_xtream_target { pub(in crate::processing) async fn playlist_resolve_download_playlist_item(client: Arc, pli: &PlaylistItem, input: &ConfigInput, errors: &mut Vec, resolve_delay: u16, cluster: XtreamCluster) -> Option { let mut result = None; let provider_id = pli.get_provider_id()?; - if let Some(info_url) = download::get_xtream_player_api_info_url(input, cluster, provider_id) { - result = match download::get_xtream_stream_info_content(client, &info_url, input).await { + if let Some(info_url) = xtream_utils::get_xtream_player_api_info_url(input, cluster, provider_id) { + result = match xtream_utils::get_xtream_stream_info_content(client, &info_url, input).await { Ok(content) => Some(content), Err(err) => { errors.push(info_err!(format!("{err}"))); diff --git a/src/utils/download.rs b/src/utils/download.rs index 17cb4c619..45d0831ff 100644 --- a/src/utils/download.rs +++ b/src/utils/download.rs @@ -1,25 +1,10 @@ -use crate::Arc; use std::borrow::Cow; -use crate::m3u_filter_error::{str_to_io_error, M3uFilterError}; -use crate::model::config::{Config, ConfigInput, ConfigTarget}; -use crate::model::playlist::{PlaylistEntry, PlaylistGroup, XtreamCluster}; -use crate::model::xmltv::TVGuide; -use crate::processing::{m3u_parser, xtream_parser}; -use crate::repository::xtream_repository::{rewrite_xtream_series_info_content, rewrite_xtream_vod_info_content, xtream_get_input_info}; -use crate::repository::xtream_repository; -use crate::utils::{file_utils, request_utils}; -use log::{debug, info}; -use std::cmp::Ordering; -use std::io::{Error}; +use crate::utils::{file_utils}; use std::path::PathBuf; use crate::debug_if_enabled; -use crate::model::api_proxy::{ProxyUserCredentials}; -const ACTION_GET_SERIES_INFO: &str = "get_series_info"; -const ACTION_GET_VOD_INFO: &str = "get_vod_info"; -const ACTION_GET_LIVE_INFO: &str = "get_live_info"; -fn prepare_file_path(persist: Option<&str>, working_dir: &str, action: &str) -> Option { +pub fn prepare_file_path(persist: Option<&str>, working_dir: &str, action: &str) -> Option { let persist_file: Option = persist.map(|persist_path| file_utils::prepare_persist_path(persist_path, action)); if persist_file.is_some() { @@ -31,185 +16,3 @@ fn prepare_file_path(persist: Option<&str>, working_dir: &str, action: &str) -> } } -pub async fn get_m3u_playlist(client: Arc, cfg: &Config, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) { - let url = input.url.clone(); - let persist_file_path = prepare_file_path(input.persist.as_deref(), working_dir, ""); - match request_utils::get_input_text_content(client, input, working_dir, &url, persist_file_path).await { - Ok(text) => { - (m3u_parser::parse_m3u(cfg, input, text.lines()), vec![]) - } - Err(err) => (vec![], vec![err]) - } -} - -pub fn get_xtream_player_api_action_url(input: &ConfigInput, action: &str) -> Option { - 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 - } -} - -pub fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: XtreamCluster, stream_id: u32) -> Option { - let (action, stream_id_field) = match cluster { - XtreamCluster::Live => (ACTION_GET_LIVE_INFO, "live_id"), - XtreamCluster::Video => (ACTION_GET_VOD_INFO, "vod_id"), - XtreamCluster::Series => (ACTION_GET_SERIES_INFO, "series_id"), - }; - get_xtream_player_api_action_url(input, action).map(|action_url| format!("{action_url}&{stream_id_field}={stream_id}")) -} - - -pub async fn get_xtream_stream_info_content(client: Arc, info_url: &str, input: &ConfigInput) -> Result { - request_utils::download_text_content(client, input, info_url, None).await -} - -#[allow(clippy::too_many_arguments)] -pub async fn get_xtream_stream_info

(client: Arc, - config: &Config, - user: &ProxyUserCredentials, - input: &ConfigInput, - target: &ConfigTarget, - pli: &P, - info_url: &str, - cluster: XtreamCluster) -> Result -where - P: PlaylistEntry, -{ - if cluster == XtreamCluster::Series { - if let Some(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id()).await { - // Deliver existing target content - return rewrite_xtream_series_info_content(config, target, pli, user, &content).await; - } - - // Check if the content has been resolved - let resolve_series = target.options.as_ref().is_some_and(|opt| opt.xtream_resolve_series); - if resolve_series { - if let Some(provider_id) = pli.get_provider_id() { - if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Series).await { - return xtream_repository::write_and_get_xtream_series_info(config, target, pli, user, &content).await; - } - } - } - } else if cluster == XtreamCluster::Video { - if let Some(content) = xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id()).await { - // Deliver existing target content - return rewrite_xtream_vod_info_content(config, target, pli, user, &content); - } - // Check if the content has been resolved - let resolve_vod = target.options.as_ref().is_some_and(|opt| opt.xtream_resolve_vod); - if resolve_vod { - if let Some(provider_id) = pli.get_provider_id() { - if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Video).await { - return xtream_repository::write_and_get_xtream_vod_info(config, target, pli, user, &content).await; - } - } - } - } - - if let Ok(content) = get_xtream_stream_info_content(client, info_url, input).await { - return match cluster { - XtreamCluster::Live => Ok(content), - XtreamCluster::Video => xtream_repository::write_and_get_xtream_vod_info(config, target, pli, user, &content).await, - XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, pli, user, &content).await, - }; - } - - Err(str_to_io_error(&format!("Cant find stream with id: {}/{}/{}", - target.name.replace(' ', "_").as_str(), &cluster, pli.get_virtual_id()))) -} - -fn get_skip_cluster(input: &ConfigInput) -> Vec { - let mut skip_cluster = vec![]; - if let Some(input_options) = &input.options { - if input_options.xtream_skip_live { - skip_cluster.push(XtreamCluster::Live); - } - if input_options.xtream_skip_vod { - skip_cluster.push(XtreamCluster::Video); - } - if input_options.xtream_skip_series { - skip_cluster.push(XtreamCluster::Series); - } - } - if skip_cluster.len() == 3 { - info!("You have skipped all sections from xtream input {}", &input.name); - } - skip_cluster -} - -const ACTIONS: [(XtreamCluster, &str, &str); 3] = [ - (XtreamCluster::Live, "get_live_categories", "get_live_streams"), - (XtreamCluster::Video, "get_vod_categories", "get_vod_streams"), - (XtreamCluster::Series, "get_series_categories", "get_series")]; - -pub async fn get_xtream_playlist(client: Arc, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) { - let mut playlist_groups: Vec = Vec::with_capacity(128); - let username = input.username.as_ref().map_or("", |v| v); - let password = input.password.as_ref().map_or("", |v| v); - let base_url = format!("{}/player_api.php?username={}&password={}", input.url, username, password); - - let skip_cluster = get_skip_cluster(input); - - let mut errors = vec![]; - for (xtream_cluster, category, stream) in &ACTIONS { - if !skip_cluster.contains(xtream_cluster) { - let category_url = format!("{base_url}&action={category}"); - let stream_url = format!("{base_url}&action={stream}"); - let category_file_path = prepare_file_path(input.persist.as_deref(), working_dir, format!("{category}_").as_str()); - let stream_file_path = prepare_file_path(input.persist.as_deref(), working_dir, format!("{stream}_").as_str()); - - match futures::join!( - request_utils::get_input_json_content(Arc::clone(&client), input, category_url.as_str(), category_file_path), - request_utils::get_input_json_content(Arc::clone(&client), input, stream_url.as_str(), stream_file_path) - ) { - (Ok(category_content), Ok(stream_content)) => { - match xtream_parser::parse_xtream(input, - *xtream_cluster, - &category_content, - &stream_content) { - Ok(sub_playlist_parsed) => { - if let Some(mut xtream_sub_playlist) = sub_playlist_parsed { - playlist_groups.append(&mut xtream_sub_playlist); - } - } - Err(err) => errors.push(err) - } - } - (Err(err1), Err(err2)) => { - errors.extend([err1, err2]); - } - (_, Err(err)) | (Err(err), _) => errors.push(err), - } - } - } - playlist_groups.sort_by(|a, b| a.title.partial_cmp(&b.title).unwrap_or(Ordering::Greater)); - - for (grp_id, plg) in (1_u32..).zip(playlist_groups.iter_mut()) { - plg.id = grp_id; - } - (playlist_groups, errors) -} - -pub async fn get_xmltv(client: Arc, _cfg: &Config, input: &ConfigInput, working_dir: &str) -> (Option, Vec) { - match &input.epg_url { - None => (None, vec![]), - Some(url) => { - debug!("Getting epg file path for url: {}", url); - let persist_file_path = prepare_file_path(input.persist.as_deref(), working_dir, "") - .map(|path| file_utils::add_prefix_to_filename(&path, "epg_", Some("xml"))); - - match request_utils::get_input_text_content_as_file(client, input, working_dir, url, persist_file_path).await { - Ok(file) => { - (Some(TVGuide { file }), vec![]) - } - Err(err) => (None, vec![err]) - } - } - } -} \ No newline at end of file diff --git a/src/utils/epg_utils.rs b/src/utils/epg_utils.rs new file mode 100644 index 000000000..2ce4ae301 --- /dev/null +++ b/src/utils/epg_utils.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; +use log::debug; +use crate::m3u_filter_error::M3uFilterError; +use crate::model::config::{Config, ConfigInput}; +use crate::model::xmltv::TVGuide; +use crate::utils::download::prepare_file_path; +use crate::utils::{file_utils, request_utils}; + +pub async fn get_xmltv(client: Arc, _cfg: &Config, input: &ConfigInput, working_dir: &str) -> (Option, Vec) { + match &input.epg_url { + None => (None, vec![]), + Some(url) => { + debug!("Getting epg file path for url: {}", url); + let persist_file_path = prepare_file_path(input.persist.as_deref(), working_dir, "") + .map(|path| file_utils::add_prefix_to_filename(&path, "epg_", Some("xml"))); + + match request_utils::get_input_text_content_as_file(client, input, working_dir, url, persist_file_path).await { + Ok(file) => { + (Some(TVGuide { file }), vec![]) + } + Err(err) => (None, vec![err]) + } + } + } +} \ No newline at end of file diff --git a/src/utils/m3u_utils.rs b/src/utils/m3u_utils.rs new file mode 100644 index 000000000..92ecf0d60 --- /dev/null +++ b/src/utils/m3u_utils.rs @@ -0,0 +1,18 @@ +use std::sync::Arc; +use crate::m3u_filter_error::M3uFilterError; +use crate::model::config::{Config, ConfigInput}; +use crate::model::playlist::PlaylistGroup; +use crate::processing::m3u_parser; +use crate::utils::download::prepare_file_path; +use crate::utils::request_utils; + +pub async fn get_m3u_playlist(client: Arc, cfg: &Config, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) { + let url = input.url.clone(); + let persist_file_path = prepare_file_path(input.persist.as_deref(), working_dir, ""); + match request_utils::get_input_text_content(client, input, working_dir, &url, persist_file_path).await { + Ok(text) => { + (m3u_parser::parse_m3u(cfg, input, text.lines()), vec![]) + } + Err(err) => (vec![], vec![err]) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 1275290fb..56803887e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -14,6 +14,9 @@ pub mod lru_cache; pub mod size_utils; pub mod sys; pub mod atomic_once_flag; +pub mod xtream_utils; +pub mod m3u_utils; +pub mod epg_utils; #[macro_export] macro_rules! debug_if_enabled { diff --git a/src/utils/request_utils.rs b/src/utils/request_utils.rs index 1a47a85e5..a6406bbf9 100644 --- a/src/utils/request_utils.rs +++ b/src/utils/request_utils.rs @@ -359,7 +359,8 @@ pub async fn get_input_json_content(client: Arc, input: &Config static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(username=)[^&]*").unwrap()); static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(password=)[^&]*").unwrap()); static TOKEN_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(token=)[^&]*").unwrap()); -static STREAM_URL: LazyLock = LazyLock::new(|| Regex::new(r"(.*://).*/(live|video|movie|series|m3u-stream)/\w+/\w+").unwrap()); +static STREAM_URL_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(.*://).*/(live|video|movie|series|m3u-stream|resource)/\w+/\w+").unwrap()); +static URL_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(.*://).*?/(.*)").unwrap()); static SANITIZE_SENSITIVE_INFO: LazyLock = LazyLock::new(|| AtomicBool::new(true)); @@ -372,7 +373,8 @@ pub fn sanitize_sensitive_info(query: &str) -> String { 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***"); - let masked_query = STREAM_URL.replace_all(&masked_query, "$1***/$2/***"); + let masked_query = STREAM_URL_REGEX.replace_all(&masked_query, "$1***/$2/***"); + let masked_query = URL_REGEX.replace_all(&masked_query, "$1***/$2"); masked_query.to_string() } else { query.to_string() @@ -397,16 +399,14 @@ pub fn extract_extension_from_url(url: &str) -> Option<&str> { #[cfg(test)] mod tests { - // use crate::utils::request_utils::{capitalize_header_name, STREAM_URL}; - - use crate::utils::request_utils::STREAM_URL; + use crate::utils::request_utils::{sanitize_sensitive_info}; #[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}") + let query = "https://bubblegum.tv/live/username/password/2344"; + let masked = sanitize_sensitive_info(&query); + println!("{masked}") } } \ No newline at end of file diff --git a/src/utils/xtream_utils.rs b/src/utils/xtream_utils.rs new file mode 100644 index 000000000..418917f40 --- /dev/null +++ b/src/utils/xtream_utils.rs @@ -0,0 +1,176 @@ +use crate::Arc; +use crate::m3u_filter_error::{str_to_io_error, M3uFilterError}; +use crate::model::config::{Config, ConfigInput, ConfigTarget}; +use crate::model::playlist::{PlaylistEntry, PlaylistGroup, XtreamCluster}; +use crate::processing::{xtream_parser}; +use crate::repository::xtream_repository::{rewrite_xtream_series_info_content, rewrite_xtream_vod_info_content, xtream_get_input_info}; +use crate::repository::xtream_repository; +use crate::utils::{request_utils}; +use log::{info}; +use std::cmp::Ordering; +use std::io::{Error}; +use crate::model::api_proxy::{ProxyUserCredentials}; +const ACTION_GET_SERIES_INFO: &str = "get_series_info"; +const ACTION_GET_VOD_INFO: &str = "get_vod_info"; +const ACTION_GET_LIVE_INFO: &str = "get_live_info"; + +#[inline] +pub fn get_xtream_stream_url_base(url: &str, username: &str, password: &str) -> String { + format!("{url}/player_api.php?username={username}&password={password}") +} + + +pub fn get_xtream_player_api_action_url(input: &ConfigInput, action: &str) -> Option { + if let Some(user_info) = input.get_user_info() { + Some(format!("{}&action={}", + get_xtream_stream_url_base( + &user_info.base_url, + &user_info.username, + &user_info.password), + action + )) + } else { + None + } +} + +pub fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: XtreamCluster, stream_id: u32) -> Option { + let (action, stream_id_field) = match cluster { + XtreamCluster::Live => (ACTION_GET_LIVE_INFO, "live_id"), + XtreamCluster::Video => (ACTION_GET_VOD_INFO, "vod_id"), + XtreamCluster::Series => (ACTION_GET_SERIES_INFO, "series_id"), + }; + get_xtream_player_api_action_url(input, action).map(|action_url| format!("{action_url}&{stream_id_field}={stream_id}")) +} + + +pub async fn get_xtream_stream_info_content(client: Arc, info_url: &str, input: &ConfigInput) -> Result { + request_utils::download_text_content(client, input, info_url, None).await +} + +#[allow(clippy::too_many_arguments)] +pub async fn get_xtream_stream_info

(client: Arc, + config: &Config, + user: &ProxyUserCredentials, + input: &ConfigInput, + target: &ConfigTarget, + pli: &P, + info_url: &str, + cluster: XtreamCluster) -> Result +where + P: PlaylistEntry, +{ + if cluster == XtreamCluster::Series { + if let Some(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id()).await { + // Deliver existing target content + return rewrite_xtream_series_info_content(config, target, pli, user, &content).await; + } + + // Check if the content has been resolved + let resolve_series = target.options.as_ref().is_some_and(|opt| opt.xtream_resolve_series); + if resolve_series { + if let Some(provider_id) = pli.get_provider_id() { + if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Series).await { + return xtream_repository::write_and_get_xtream_series_info(config, target, pli, user, &content).await; + } + } + } + } else if cluster == XtreamCluster::Video { + if let Some(content) = xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id()).await { + // Deliver existing target content + return rewrite_xtream_vod_info_content(config, target, pli, user, &content); + } + // Check if the content has been resolved + let resolve_vod = target.options.as_ref().is_some_and(|opt| opt.xtream_resolve_vod); + if resolve_vod { + if let Some(provider_id) = pli.get_provider_id() { + if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Video).await { + return xtream_repository::write_and_get_xtream_vod_info(config, target, pli, user, &content).await; + } + } + } + } + + if let Ok(content) = get_xtream_stream_info_content(client, info_url, input).await { + return match cluster { + XtreamCluster::Live => Ok(content), + XtreamCluster::Video => xtream_repository::write_and_get_xtream_vod_info(config, target, pli, user, &content).await, + XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, pli, user, &content).await, + }; + } + + Err(str_to_io_error(&format!("Cant find stream with id: {}/{}/{}", + target.name.replace(' ', "_").as_str(), &cluster, pli.get_virtual_id()))) +} + +fn get_skip_cluster(input: &ConfigInput) -> Vec { + let mut skip_cluster = vec![]; + if let Some(input_options) = &input.options { + if input_options.xtream_skip_live { + skip_cluster.push(XtreamCluster::Live); + } + if input_options.xtream_skip_vod { + skip_cluster.push(XtreamCluster::Video); + } + if input_options.xtream_skip_series { + skip_cluster.push(XtreamCluster::Series); + } + } + if skip_cluster.len() == 3 { + info!("You have skipped all sections from xtream input {}", &input.name); + } + skip_cluster +} + +const ACTIONS: [(XtreamCluster, &str, &str); 3] = [ + (XtreamCluster::Live, "get_live_categories", "get_live_streams"), + (XtreamCluster::Video, "get_vod_categories", "get_vod_streams"), + (XtreamCluster::Series, "get_series_categories", "get_series")]; + +pub async fn get_xtream_playlist(client: Arc, input: &ConfigInput, working_dir: &str) -> (Vec, Vec) { + let mut playlist_groups: Vec = Vec::with_capacity(128); + let username = input.username.as_ref().map_or("", |v| v); + let password = input.password.as_ref().map_or("", |v| v); + let base_url = format!("{}/player_api.php?username={}&password={}", input.url, username, password); + + let skip_cluster = get_skip_cluster(input); + + let mut errors = vec![]; + for (xtream_cluster, category, stream) in &ACTIONS { + if !skip_cluster.contains(xtream_cluster) { + let category_url = format!("{base_url}&action={category}"); + let stream_url = format!("{base_url}&action={stream}"); + let category_file_path = crate::utils::download::prepare_file_path(input.persist.as_deref(), working_dir, format!("{category}_").as_str()); + let stream_file_path = crate::utils::download::prepare_file_path(input.persist.as_deref(), working_dir, format!("{stream}_").as_str()); + + match futures::join!( + request_utils::get_input_json_content(Arc::clone(&client), input, category_url.as_str(), category_file_path), + request_utils::get_input_json_content(Arc::clone(&client), input, stream_url.as_str(), stream_file_path) + ) { + (Ok(category_content), Ok(stream_content)) => { + match xtream_parser::parse_xtream(input, + *xtream_cluster, + &category_content, + &stream_content) { + Ok(sub_playlist_parsed) => { + if let Some(mut xtream_sub_playlist) = sub_playlist_parsed { + playlist_groups.append(&mut xtream_sub_playlist); + } + } + Err(err) => errors.push(err) + } + } + (Err(err1), Err(err2)) => { + errors.extend([err1, err2]); + } + (_, Err(err)) | (Err(err), _) => errors.push(err), + } + } + } + playlist_groups.sort_by(|a, b| a.title.partial_cmp(&b.title).unwrap_or(Ordering::Greater)); + + for (grp_id, plg) in (1_u32..).zip(playlist_groups.iter_mut()) { + plg.id = grp_id; + } + (playlist_groups, errors) +} \ No newline at end of file