diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 7a3982e23..c7d944410 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -197,6 +197,9 @@ pub fn is_stream_share_enabled(item_type: PlaylistItemType, target: &ConfigTarge } pub async fn resource_response(_app_state: &AppState, resource_url: &str, req: &HttpRequest, input: Option<&ConfigInput>) -> HttpResponse { + if resource_url.is_empty() { + return HttpResponse::NoContent().finish(); + } let req_headers: HashMap<&str, &[u8]> = req.headers().iter().map(|(k, v)| (k.as_str(), v.as_bytes())).collect(); debug_if_enabled!("Try to open resource {}", mask_sensitive_info(resource_url)); diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index 0589cdb11..79a360474 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -14,7 +14,7 @@ use log::{debug, error, warn}; use serde_json::{Map, Value}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, is_stream_share_enabled, serve_file, stream_response}; +use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, is_stream_share_enabled, resource_response, serve_file, stream_response}; use crate::api::model::app_state::AppState; use crate::api::model::request::UserApiRequest; use crate::api::model::xtream::XtreamAuthorizationResponse; @@ -22,7 +22,7 @@ use crate::{debug_if_enabled, info_err}; use crate::model::api_proxy::{ProxyType, ProxyUserCredentials}; use crate::model::config::TargetType; use crate::model::config::{Config, ConfigInput, ConfigTarget}; -use crate::model::playlist::{PlaylistItemType, XtreamCluster}; +use crate::model::playlist::{FieldGetAccessor, PlaylistItemType, XtreamCluster}; use crate::repository::storage::{get_target_storage_path, hash_string}; use crate::repository::target_id_mapping::TargetIdMapping; use crate::repository::xtream_repository; @@ -216,6 +216,37 @@ async fn xtream_player_api_stream( stream_response(app_state, &stream_url, req, Some(input), share_live_streams).await } +async fn xtream_player_api_resource( + req: &HttpRequest, + api_req: &web::Query, + app_state: &web::Data, + resource_req: XtreamApiStreamRequest<'_>, +) -> HttpResponse { + let (user, target) = try_option_bad_request!(get_user_target_by_credentials(resource_req.username, resource_req.password, api_req, app_state), false, format!("Could not find any user {}", resource_req.username)); + let target_name = &target.name; + if !target.has_output(&TargetType::Xtream) { + debug!("Target has no xtream output {}", target_name); + return HttpResponse::BadRequest().finish(); + } + let virtual_id: u32 = try_result_bad_request!(resource_req.stream_id.trim().parse()); + let pli = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None).await, true, format!("Failed to read xtream item for stream id {}", virtual_id)); + // let input = try_option_bad_request!(app_state.config.get_input_by_id(pli.input_id), true, format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", resource_req.context)); + + let resource = resource_req.action_path.trim(); + let stream_url = pli.get_field(resource); + match stream_url { + None => HttpResponse::NotFound().finish(), + Some(url) => { + if user.proxy == ProxyType::Redirect { + debug!("Redirecting stream request to {}", mask_sensitive_info(&url)); + HttpResponse::Found().insert_header(("Location", url.as_str())).finish() + } else { + resource_response(app_state, url.as_str(), req, None).await + } + } + } +} + macro_rules! create_xtream_player_api_stream { ($fn_name:ident, $context:expr) => { async fn $fn_name( @@ -230,11 +261,29 @@ macro_rules! create_xtream_player_api_stream { } } +macro_rules! create_xtream_player_api_resource{ + ($fn_name:ident, $context:expr) => { + async fn $fn_name( + req: HttpRequest, + api_req: web::Query, + path: web::Path<(String, String, String, String)>, + app_state: web::Data, + ) -> HttpResponse { + let (username, password, stream_id, resource) = path.into_inner(); + xtream_player_api_resource(&req, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, &resource)).await + } + } +} + create_xtream_player_api_stream!(xtream_player_api_live_stream, XtreamApiStreamContext::Live); create_xtream_player_api_stream!(xtream_player_api_live_stream_alt, XtreamApiStreamContext::LiveAlt); create_xtream_player_api_stream!(xtream_player_api_series_stream, XtreamApiStreamContext::Series); create_xtream_player_api_stream!(xtream_player_api_movie_stream, XtreamApiStreamContext::Movie); +create_xtream_player_api_resource!(xtream_player_api_live_resource, XtreamApiStreamContext::Live); +create_xtream_player_api_resource!(xtream_player_api_series_resource, XtreamApiStreamContext::Series); +create_xtream_player_api_resource!(xtream_player_api_movie_resource, XtreamApiStreamContext::Movie); + fn get_non_empty<'a>(first: &'a str, second: &'a str, third: &'a str) -> &'a str { if !first.is_empty() { first @@ -446,11 +495,11 @@ async fn xtream_player_api( let category_id = api_req.category_id.trim().parse::().unwrap_or(0); let result = match action { ACTION_GET_LIVE_STREAMS => - skip_flag_optional!(skip_live, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Live, &app_state.config, target, category_id).await), + skip_flag_optional!(skip_live, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Live, &app_state.config, target, category_id, &user).await), ACTION_GET_VOD_STREAMS => - skip_flag_optional!(skip_vod, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Video, &app_state.config, target, category_id).await), + skip_flag_optional!(skip_vod, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Video, &app_state.config, target, category_id, &user).await), ACTION_GET_SERIES => - skip_flag_optional!(skip_series, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Series, &app_state.config, target, category_id).await), + skip_flag_optional!(skip_series, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Series, &app_state.config, target, category_id, &user).await), _ => Some(Err(info_err!(format!("Cant find action: {action} for target: {}", &target.name)) )), }; @@ -532,6 +581,14 @@ macro_rules! register_xtream_api_stream { }}; } +macro_rules! register_xtream_api_resource { + ($cfg:expr, [$(($path:expr, $fn_name:ident)),*]) => {{ + $( + $cfg.service(web::resource(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path)).route(web::get().to($fn_name))); + )* + }}; +} + macro_rules! register_xtream_api_timeshift { ($cfg:expr, [$($path:expr),*]) => {{ $( @@ -551,6 +608,10 @@ pub fn xtream_api_register(cfg: &mut web::ServiceConfig) { "/timeshift/{username}/{password}/{duration}/{start}/{stream_id}", "/timeshift.php", "/streaming/timeshift.php"]); + register_xtream_api_resource!(cfg, [ + ("live", xtream_player_api_live_resource), + ("movie", xtream_player_api_movie_resource), + ("series", xtream_player_api_series_resource)]); /* TODO cfg.service(web::resource("/hlsr/{token}/{username}/{password}/{channel}/{hash}/{chunk}").route(web::get().to(xtream_player_api_hlsr_stream))); cfg.service(web::resource("/hls/{token}/{chunk}").route(web::get().to(xtream_player_api_hls_stream))); diff --git a/src/model/playlist.rs b/src/model/playlist.rs index b35de9c5d..2186afd0d 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -3,15 +3,15 @@ use std::cmp::PartialEq; use std::fmt::{Display, Formatter}; use std::rc::Rc; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - +use crate::model::api_proxy::ProxyUserCredentials; use crate::model::config::{ConfigInput, ConfigTargetOptions}; use crate::model::xmltv::TVGuide; use crate::model::xtream::{xtream_playlistitem_to_document, XtreamMappingOptions}; use crate::processing::m3u_parser::extract_id_from_url; use crate::repository::storage::hash_string; use crate::utils::json_utils::{get_string_from_serde_value, get_u64_from_serde_value}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html @@ -57,6 +57,13 @@ impl XtreamCluster { Self::Series => "Series", } } + pub const fn as_stream_type(&self) -> &str { + match self { + Self::Live => "live", + Self::Video => "movie", + Self::Series => "series", + } + } } impl Display for XtreamCluster { @@ -127,7 +134,7 @@ pub trait FieldSetAccessor { fn set_field(&mut self, field: &str, value: &str) -> bool; } -pub type UUIDType = [u8;32]; +pub type UUIDType = [u8; 32]; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct PlaylistItemHeader { @@ -300,7 +307,7 @@ impl M3uPlaylistItem { match resource_url { None => { to_m3u_non_empty_fields!(self, line, (logo, "tvg-logo"), (logo_small, "tvg-logo-small");); - }, + } Some(res_url) => { to_m3u_resource_non_empty_fields!(self, res_url, line, (logo, "tvg-logo"), (logo_small, "tvg-logo-small");); } @@ -319,7 +326,6 @@ impl M3uPlaylistItem { } impl PlaylistEntry for M3uPlaylistItem { - #[inline] fn get_virtual_id(&self) -> u32 { self.virtual_id @@ -388,13 +394,12 @@ pub struct XtreamPlaylistItem { } impl XtreamPlaylistItem { - pub fn to_doc(&self, options: &XtreamMappingOptions) -> Value { - xtream_playlistitem_to_document(self, options) + pub fn to_doc(&self, url: &str, options: &XtreamMappingOptions, user: &ProxyUserCredentials) -> Value { + xtream_playlistitem_to_document(self, url, options, user) } } impl PlaylistEntry for XtreamPlaylistItem { - #[inline] fn get_virtual_id(&self) -> u32 { self.virtual_id @@ -411,8 +416,61 @@ impl PlaylistEntry for XtreamPlaylistItem { fn get_provider_url(&self) -> Rc { Rc::clone(&self.url) } +} + +fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option> { + match value { + Some(Value::String(url)) => Some(Rc::new(url.clone())), + Some(Value::Array(values)) => { + match values.as_slice() { + [single] => Some(Rc::new(single.to_string())), + multiple if !multiple.is_empty() => { + if let Some(index) = field.rfind('_') { + if let Ok(bd_index) = field[index + 1..].parse::() { + if let Some(selected) = multiple.get(bd_index) { + return Some(Rc::new(selected.to_string())); + } + } + } + Some(Rc::new(multiple[0].to_string())) + } + _ => None, + } + } + _ => None, + } +} +macro_rules! generate_field_accessor_impl_for_xtream_playlist_item { + ($($prop:ident),*;) => { + impl FieldGetAccessor for XtreamPlaylistItem { + fn get_field(&self, field: &str) -> Option> { + match field { + $( + stringify!($prop) => Some(self.$prop.clone()), + )* + "epg_channel_id" | "epg_id" => self.epg_channel_id.clone(), + _ => { + if field.starts_with("bakdrop_path") || field == "cover" { + let props = self.additional_properties.as_ref().and_then(|add_props| serde_json::from_str::>(add_props).ok()); + return match props { + Some(doc) => { + return if field == "cover" { + doc.get("cover").and_then(|value| value.as_str().map(|s| Rc::new(s.to_string()))) + } else { + get_backdrop_path_value(field, doc.get("backdrop_path")) + } + } + _=> None, + } + } + None + }, + } + } + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -420,13 +478,15 @@ pub struct PlaylistItem { pub header: RefCell, } +generate_field_accessor_impl_for_xtream_playlist_item!(name, logo, logo_small, group,title, parent_code, rec, url;); + impl PlaylistItem { pub fn to_m3u(&self) -> M3uPlaylistItem { let header = self.header.borrow(); M3uPlaylistItem { virtual_id: header.virtual_id, provider_id: Rc::clone(&header.id), - name: Rc::clone(if header.item_type == PlaylistItemType::Series {&header.title} else {&header.name}), + name: Rc::clone(if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }), chno: Rc::clone(&header.chno), logo: Rc::clone(&header.logo), logo_small: Rc::clone(&header.logo_small), @@ -449,7 +509,7 @@ impl PlaylistItem { XtreamPlaylistItem { virtual_id: header.virtual_id, provider_id, - name: Rc::clone(if header.item_type == PlaylistItemType::Series {&header.title} else {&header.name}), + name: Rc::clone(if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }), logo: Rc::clone(&header.logo), logo_small: Rc::clone(&header.logo_small), group: Rc::clone(&header.group), @@ -468,7 +528,6 @@ impl PlaylistItem { } impl PlaylistEntry for PlaylistItem { - #[inline] fn get_virtual_id(&self) -> u32 { self.header.borrow().virtual_id @@ -498,7 +557,6 @@ impl PlaylistEntry for PlaylistItem { fn get_provider_url(&self) -> Rc { Rc::clone(&self.header.borrow().url) } - } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 1a4bb5c41..d0dacbf75 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -2,13 +2,16 @@ use std::collections::HashMap; use std::iter::FromIterator; use std::rc::Rc; +use crate::model::api_proxy::{ProxyType, ProxyUserCredentials}; +use crate::model::config::ConfigTargetOptions; +use crate::model::playlist::{PlaylistEntry, PlaylistItem, XtreamCluster, XtreamPlaylistItem}; use crate::utils::json_utils::{opt_string_or_number_u32, string_default_on_null, string_or_number_f64, string_or_number_u32}; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; -use crate::model::config::ConfigTargetOptions; -use crate::model::playlist::{PlaylistItem, XtreamCluster, XtreamPlaylistItem}; +const PROP_BACKDROP_PATH: &str = "backdrop_path"; +const PROP_COVER: &str = "cover"; const LIVE_STREAM_FIELDS: &[&str] = &[]; @@ -20,11 +23,12 @@ const VIDEO_STREAM_FIELDS: &[&str] = &[ ]; const SERIES_STREAM_FIELDS: &[&str] = &[ - "backdrop_path", "cast", "cover", "director", "episode_run_time", "genre", + PROP_BACKDROP_PATH, "cast", PROP_COVER, "director", "episode_run_time", "genre", "last_modified", "name", "plot", "rating_5based", "stream_type", "title", "year", "youtube_trailer", ]; + fn deserialize_number_from_string<'de, D, T: DeserializeOwned>( deserializer: D, ) -> Result, D::Error> @@ -100,6 +104,7 @@ where D: Deserializer<'de>, { Value::deserialize(deserializer).map(|v| match v { + Value::String(value) => Some(vec![value]), Value::Array(value) => Some(value_to_string_array(&value)), _ => None, }) @@ -522,15 +527,34 @@ fn append_prepared_series_properties(add_props: Option<&Map>, doc } } -pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, options: &XtreamMappingOptions) -> serde_json::Value { +fn make_bdpath_resource_url(resource_url: &str, bd_path: &str, index: usize) -> String { + if bd_path.starts_with("http") { + format!("{resource_url}/bd_path_{index}") + } else { + bd_path.to_string() + } +} + +pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, options: &XtreamMappingOptions, user: &ProxyUserCredentials) -> serde_json::Value { let stream_id_value = Value::Number(serde_json::Number::from(pli.virtual_id)); + let (resource_url, logo, logo_small) = match user.proxy { + ProxyType::Reverse => { + let resource_url = format!("{url}/resource/{}/{}/{}/{}", pli.xtream_cluster.as_stream_type(), user.username, user.password, pli.get_virtual_id()); + let logo_url = if pli.logo.is_empty() { String::new() } else { format!("{resource_url}/logo") }; + let logo_small_url = if pli.logo_small.is_empty() { String::new() } else { format!("{resource_url}/logo_small") }; + (Some(resource_url), logo_url, logo_small_url) + } + ProxyType::Redirect => { + (None, pli.logo.as_ref().clone(), pli.logo_small.as_ref().clone()) + } + }; let mut document = serde_json::Map::from_iter([ ("category_id".to_string(), Value::String(format!("{}", &pli.category_id))), ("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from(pli.category_id))]))), ("name".to_string(), Value::String(pli.name.as_ref().clone())), ("num".to_string(), stream_id_value.clone()), ("title".to_string(), Value::String(pli.title.as_ref().clone())), - ("stream_icon".to_string(), Value::String(pli.logo.as_ref().clone())), + ("stream_icon".to_string(), Value::String(logo)), ]); match pli.xtream_cluster { @@ -541,7 +565,7 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, options: &Xtrea } else { document.insert("direct_source".to_string(), Value::String(pli.url.as_ref().clone())); } - document.insert("thumbnail".to_string(), Value::String(pli.logo_small.as_ref().clone())); + document.insert("thumbnail".to_string(), Value::String(logo_small)); document.insert("custom_sid".to_string(), Value::String(String::new())); document.insert("epg_channel_id".to_string(), pli.epg_channel_id.as_ref().map_or(Value::Null, |epg_id| Value::String(epg_id.as_ref().clone()))); } @@ -584,5 +608,30 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, options: &Xtrea append_release_date(&mut document); } }; + + if let Some(rewrite_url) = resource_url { + if let Some(bdpath) = document.get(PROP_BACKDROP_PATH) { + match bdpath { + Value::String(bd_path) => { + document.insert(PROP_BACKDROP_PATH.to_string(), Value::Array(vec![Value::String(make_bdpath_resource_url(rewrite_url.as_str(), bd_path, 0))])); + } + Value::Array(bd_path) => { + document.insert(PROP_BACKDROP_PATH.to_string(), Value::Array( + bd_path.iter() + .filter_map(|val| val.as_str()) + .enumerate() + .map(|(index, value)| Value::String(make_bdpath_resource_url(rewrite_url.as_str(), value, index))) + .collect())); + } + _ => {} + }; + } + if let Some(Value::String(cover)) = document.get(PROP_COVER) { + if cover.starts_with("http") { + document.insert(PROP_COVER.to_string(), Value::String(format!("{}/cover", rewrite_url.as_str()))); + } + } + } + Value::Object(document) } diff --git a/src/processing/xtream_parser.rs b/src/processing/xtream_parser.rs index 7a146120a..64f161242 100644 --- a/src/processing/xtream_parser.rs +++ b/src/processing/xtream_parser.rs @@ -50,23 +50,24 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s let result: Vec<(XtreamSeriesInfoEpisode, PlaylistItem)> = episodes.values().flatten().map(|episode| { let episode_url = create_xtream_series_info_url(url, username, password, episode); (episode.clone(), - PlaylistItem { - header: RefCell::new(PlaylistItemHeader { - id: Rc::new(episode.id.to_string()), - uuid: Rc::new(hash_string(&episode_url)), - name: Rc::new(series_name.to_string()), - logo: Rc::new(episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string())), - group: Rc::new(group_title.to_string()), - title: Rc::new(episode.title.clone()), - url: episode_url, - item_type: PlaylistItemType::Series, - xtream_cluster: XtreamCluster::Series, - additional_properties: episode.get_additional_properties(&series_info), - category_id: 0, - input_id: input.id, - ..Default::default() - }) - })}).collect(); + PlaylistItem { + header: RefCell::new(PlaylistItemHeader { + id: Rc::new(episode.id.to_string()), + uuid: Rc::new(hash_string(&episode_url)), + name: Rc::new(series_name.to_string()), + logo: Rc::new(episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string())), + group: Rc::new(group_title.to_string()), + title: Rc::new(episode.title.clone()), + url: episode_url, + item_type: PlaylistItemType::Series, + xtream_cluster: XtreamCluster::Series, + additional_properties: episode.get_additional_properties(&series_info), + category_id: 0, + input_id: input.id, + ..Default::default() + }) + }) + }).collect(); return if result.is_empty() { Ok(None) } else { Ok(Some(result)) }; } Ok(None) @@ -80,24 +81,24 @@ 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 { if stream.direct_source.is_empty() { let stream_base_url = match xtream_cluster { - XtreamCluster::Live => format!("{}/live/{}/{}/{}.ts", url, username, password, &stream.get_stream_id()), + XtreamCluster::Live => format!("{url}/live/{username}/{password}/{}.ts", &stream.get_stream_id()), XtreamCluster::Video => { let ext = stream.container_extension.as_ref().map_or("mp4", |e| e.as_str()); - format!("{}/movie/{}/{}/{}.{}", url, username, password, &stream.get_stream_id(), ext) + format!("{url}/movie/{username}/{password}/{}.{ext}", &stream.get_stream_id()) } XtreamCluster::Series => - format!("{}/player_api.php?username={}&password={}&action=get_series_info&series_id={}", - url, username, password, &stream.get_stream_id()) + format!("{url}/player_api.php?username={username}&password={password}&action=get_series_info&series_id={}", &stream.get_stream_id()) }; Rc::new(stream_base_url) } else { Rc::clone(&stream.direct_source) } } + pub fn parse_xtream(input: &ConfigInput, - xtream_cluster: XtreamCluster, - categories: &Value, - streams: &Value) -> Result>, M3uFilterError> { + xtream_cluster: XtreamCluster, + categories: &Value, + streams: &Value) -> Result>, M3uFilterError> { match map_to_xtream_category(categories) { Ok(xtream_categories) => { let input_id = input.id; @@ -148,14 +149,14 @@ pub fn parse_xtream(input: &ConfigInput, Ok(Some(group_map.values().filter(|category| !category.borrow().channels.is_empty()) .map(|category| { - let cat = category.borrow(); - PlaylistGroup { - id: cat.category_id.parse::().unwrap_or(0), - xtream_cluster, - title: Rc::clone(&cat.category_name), - channels: cat.channels.clone(), - } - }).collect())) + let cat = category.borrow(); + PlaylistGroup { + id: cat.category_id.parse::().unwrap_or(0), + xtream_cluster, + title: Rc::clone(&cat.category_name), + channels: cat.channels.clone(), + } + }).collect())) } Err(err) => Err(err) } diff --git a/src/repository/xtream_playlist_iterator.rs b/src/repository/xtream_playlist_iterator.rs index 510cd9bc7..d0aa88921 100644 --- a/src/repository/xtream_playlist_iterator.rs +++ b/src/repository/xtream_playlist_iterator.rs @@ -1,6 +1,7 @@ use log::error; use crate::info_err; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; +use crate::model::api_proxy::{ProxyUserCredentials}; use crate::model::config::{Config, ConfigTarget}; use crate::model::playlist::{XtreamCluster, XtreamPlaylistItem}; use crate::model::xtream::XtreamMappingOptions; @@ -13,6 +14,8 @@ pub struct XtreamPlaylistIterator { options: XtreamMappingOptions, category_id: u32, _file_lock: FileReadGuard, + base_url: String, + user: ProxyUserCredentials, } impl XtreamPlaylistIterator { @@ -21,6 +24,7 @@ impl XtreamPlaylistIterator { config: &Config, target: &ConfigTarget, category_id: u32, + user: &ProxyUserCredentials ) -> Result { if let Some(storage_path) = xtream_get_storage_path(config, target.name.as_str()) { let (xtream_path, idx_path) = xtream_get_file_paths(&storage_path, cluster); @@ -31,12 +35,14 @@ impl XtreamPlaylistIterator { .map_err(|err| info_err!(format!("Could not deserialize file {} - {}", &xtream_path.to_str().unwrap(), err)))?; let options = XtreamMappingOptions::from_target_options(target.options.as_ref()); - + let server_info = config.get_user_server_info(user); Ok(Self { reader, options, category_id, _file_lock: file_lock, + base_url: server_info.get_base_url(), + user: user.clone(), }) } else { Err(info_err!(format!("Failed to find xtream storage for target {}", &target.name))) @@ -53,6 +59,6 @@ impl Iterator for XtreamPlaylistIterator { return None; } self.reader.find(|pli| self.category_id == 0 || pli.category_id == self.category_id) - .map(|pli| pli.to_doc(&self.options).to_string()) + .map(|pli| pli.to_doc(&self.base_url, &self.options, &self.user).to_string()) } } \ No newline at end of file diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index 2f5846ece..d08eece34 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -19,6 +19,7 @@ use crate::repository::xtream_playlist_iterator::XtreamPlaylistIterator; use crate::utils::file_utils::open_readonly_file; use crate::utils::json_utils::{json_iter_array, json_write_documents_to_file}; use crate::{create_m3u_filter_error, create_m3u_filter_error_result, info_err, notify_err}; +use crate::model::api_proxy::{ ProxyUserCredentials}; pub static COL_CAT_LIVE: &str = "cat_live"; pub static COL_CAT_SERIES: &str = "cat_series"; @@ -414,8 +415,9 @@ pub async fn xtream_load_rewrite_playlist( config: &Config, target: &ConfigTarget, category_id: u32, + user: &ProxyUserCredentials, ) -> Result>, M3uFilterError> { - Ok(Box::new(XtreamPlaylistIterator::new(cluster, config, target, category_id).await?)) + Ok(Box::new(XtreamPlaylistIterator::new(cluster, config, target, category_id, user).await?)) } pub async fn xtream_write_series_info( diff --git a/test/rest-api.http b/test/rest-api.http index 15c3f53bb..035736a45 100644 --- a/test/rest-api.http +++ b/test/rest-api.http @@ -43,22 +43,37 @@ GET {{local}}/player_api.php?username=xt&password=xt ### xtream live_categories GET {{local}}/player_api.php?username=xt&password=xt&action=get_live_categories -### xtream live_streams +### xtream live_streams reverse +GET {{local}}/player_api.php?username=xtr&password=xtr&action=get_live_streams&category_id=102 + +### xtream live_streams redirect GET {{local}}/player_api.php?username=xt&password=xt&action=get_live_streams&category_id=102 ### xtream vod_categories GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_categories -### xtream vod streams +### xtream vod streams reverse +GET {{local}}/player_api.php?username=xtr&password=xtr&action=get_vod_streams&category_id=636 + +### xtream vod streams redirect GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_streams&category_id=636 -### xtream vod info +### xtream vod info reverse +GET {{local}}/player_api.php?username=xtr&password=xtr&action=get_vod_info&vod_id=7590 + +### xtream vod info redirect GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_info&vod_id=7590 -### xtream series_categories +### xtream series_categories reverse +GET {{local}}/player_api.php?username=xtr&password=xtr&action=get_series_categories + +### xtream series_categories redirect GET {{local}}/player_api.php?username=xt&password=xt&action=get_series_categories -### xtream series +### xtream series reverse +GET {{local}}/player_api.php?username=xtr&password=xtr&action=get_series + +### xtream series redirect GET {{local}}/player_api.php?username=xt&password=xt&action=get_series ### xtream series for category