2023-10-09 00:17:54 +02:00
// https://github.com/tellytv/go.xtream-codes/blob/master/structs.go
2023-12-01 21:38:23 +01:00
use std ::collections ::HashMap ;
2024-05-05 19:15:54 +02:00
use std ::fmt ::{ Display , Formatter };
2024-05-06 10:22:35 +02:00
use std ::io ::{ Error , ErrorKind };
2024-03-26 10:26:12 +01:00
use std ::path ::Path ;
2025-01-05 18:22:23 +01:00
use std ::rc ::Rc ;
2023-11-22 21:06:58 +01:00
use std ::str ::FromStr ;
2024-05-06 10:22:35 +02:00
2024-12-05 19:54:34 +01:00
use actix_web ::{ web , HttpRequest , HttpResponse };
2024-11-01 11:14:04 +01:00
use bytes ::Bytes ;
2024-11-01 19:38:08 +01:00
use futures ::stream ::{ self , StreamExt };
2024-12-05 19:54:34 +01:00
use futures ::Stream ;
2025-01-02 08:18:16 +01:00
use log ::{ debug , error , warn };
2024-05-06 10:22:35 +02:00
use serde_json ::{ Map , Value };
2023-10-20 12:18:44 +02:00
2025-01-03 16:49:24 +01:00
use crate ::api ::api_utils ::{ get_user_target , get_user_target_by_credentials , is_stream_share_enabled , resource_response , serve_file , stream_response };
2024-12-05 19:54:34 +01:00
use crate ::api ::model ::app_state ::AppState ;
use crate ::api ::model ::request ::UserApiRequest ;
use crate ::api ::model ::xtream ::XtreamAuthorizationResponse ;
2025-01-05 18:22:23 +01:00
use crate ::m3u_filter_error ::{ M3uFilterError , M3uFilterErrorKind };
2024-05-03 01:47:41 +02:00
use crate ::model ::api_proxy ::{ ProxyType , ProxyUserCredentials };
2024-05-06 10:22:35 +02:00
use crate ::model ::config ::TargetType ;
2024-12-05 19:54:34 +01:00
use crate ::model ::config ::{ Config , ConfigInput , ConfigTarget };
2025-01-05 18:22:23 +01:00
use crate ::model ::playlist ::{ get_backdrop_path_value , FieldGetAccessor , PlaylistEntry , PlaylistItemType , XtreamCluster , XtreamPlaylistItem };
use crate ::model ::xtream ::{ INFO_RESOURCE_PREFIX , INFO_RESOURCE_PREFIX_EPISODE , PROP_BACKDROP_PATH };
2024-10-29 17:43:04 +01:00
use crate ::repository ::storage ::{ get_target_storage_path , hash_string };
use crate ::repository ::target_id_mapping ::TargetIdMapping ;
2024-03-26 10:26:12 +01:00
use crate ::repository ::xtream_repository ;
2025-01-05 18:22:23 +01:00
use crate ::repository ::xtream_repository ::{ TAG_EPISODES , TAG_INFO_DATA };
use crate ::utils ::json_utils ::get_u32_from_serde_value ;
2024-12-31 12:25:48 +01:00
use crate ::utils ::request_utils ::{ extract_extension_from_url , mask_sensitive_info };
2024-12-10 23:16:04 +01:00
use crate ::utils ::{ download , json_utils , request_utils };
2025-01-05 18:22:23 +01:00
use crate ::{ debug_if_enabled , info_err };
2024-03-26 10:26:12 +01:00
2024-11-01 11:14:04 +01:00
const ACTION_GET_SERIES_INFO : & str = "get_series_info" ;
const ACTION_GET_VOD_INFO : & str = "get_vod_info" ;
2024-12-10 23:16:04 +01:00
// const ACTION_GET_LIVE_INFO: &str = "get_live_info";
2024-11-01 11:14:04 +01:00
const ACTION_GET_EPG : & str = "get_epg" ;
const ACTION_GET_SHORT_EPG : & str = "get_short_epg" ;
const ACTION_GET_CATCHUP_TABLE : & str = "get_simple_data_table" ;
const ACTION_GET_LIVE_CATEGORIES : & str = "get_live_categories" ;
const ACTION_GET_VOD_CATEGORIES : & str = "get_vod_categories" ;
2024-12-05 19:54:34 +01:00
const ACTION_GET_SERIES_CATEGORIES : & str = "get_series_categories" ;
2024-11-01 11:14:04 +01:00
const ACTION_GET_LIVE_STREAMS : & str = "get_live_streams" ;
const ACTION_GET_VOD_STREAMS : & str = "get_vod_streams" ;
const ACTION_GET_SERIES : & str = "get_series" ;
const TAG_ID : & str = "id" ;
const TAG_CATEGORY_ID : & str = "category_id" ;
const TAG_STREAM_ID : & str = "stream_id" ;
const TAG_EPG_LISTINGS : & str = "epg_listings" ;
2024-10-29 17:43:04 +01:00
macro_rules ! try_option_bad_request {
( $option :expr , $msg_is_error :expr , $msg :expr ) => {
match $option {
Some ( value ) => value ,
None => {
if $msg_is_error { error! ( "{}" , $msg );} else { debug! ( "{}" , $msg );}
return HttpResponse ::BadRequest (). finish ();
}
}
};
( $option :expr ) => {
match $option {
Some ( value ) => value ,
None => return HttpResponse ::BadRequest (). finish (),
}
};
}
macro_rules ! try_result_bad_request {
( $option :expr , $msg_is_error :expr , $msg :expr ) => {
match $option {
Ok ( value ) => value ,
Err ( _ ) => {
if $msg_is_error { error! ( "{}" , $msg );} else { debug! ( "{}" , $msg );}
return HttpResponse ::BadRequest (). finish ();
}
}
};
( $option :expr ) => {
match $option {
Ok ( value ) => value ,
Err ( _ ) => return HttpResponse ::BadRequest (). finish (),
}
};
}
2024-05-10 10:13:50 +02:00
enum XtreamApiStreamContext {
LiveAlt ,
Live ,
Movie ,
Series ,
Timeshift ,
}
2024-11-01 11:14:04 +01:00
impl XtreamApiStreamContext {
const LIVE_ALT : & 'static str = "" ;
const LIVE : & 'static str = "live" ;
const MOVIE : & 'static str = "movie" ;
const SERIES : & 'static str = "series" ;
const TIMESHIFT : & 'static str = "timeshift" ;
}
2024-05-10 10:13:50 +02:00
impl Display for XtreamApiStreamContext {
fn fmt ( & self , f : & mut Formatter < '_ > ) -> std ::fmt ::Result {
write! ( f , "{}" , match self {
2024-11-04 18:46:56 +01:00
Self ::LiveAlt => Self ::LIVE_ALT ,
Self ::Live => Self ::LIVE ,
Self ::Movie => Self ::MOVIE ,
Self ::Series => Self ::SERIES ,
Self ::Timeshift => Self ::TIMESHIFT ,
2024-05-10 10:13:50 +02:00
})
}
}
struct XtreamApiStreamRequest < 'a > {
context : XtreamApiStreamContext ,
username : & 'a str ,
password : & 'a str ,
stream_id : & 'a str ,
action_path : & 'a str ,
}
impl < 'a > XtreamApiStreamRequest < 'a > {
2024-11-04 18:46:56 +01:00
pub const fn from ( context : XtreamApiStreamContext ,
2024-12-05 19:54:34 +01:00
username : & 'a str ,
password : & 'a str ,
stream_id : & 'a str ,
action_path : & 'a str ) -> Self {
2024-05-10 10:13:50 +02:00
Self {
context ,
username ,
password ,
stream_id ,
action_path ,
}
}
}
2024-11-04 18:46:56 +01:00
pub fn serve_query ( file_path : & Path , filter : & HashMap <& str , & str > ) -> HttpResponse {
2024-05-07 20:27:07 +02:00
let filtered = json_utils ::json_filter_file ( file_path , filter );
2024-03-26 10:26:12 +01:00
HttpResponse ::Ok (). json ( filtered )
}
2023-12-01 21:38:23 +01:00
2024-11-10 14:17:24 +01:00
fn get_xtream_player_api_stream_url ( input : & ConfigInput , context : & str , action_path : & str , fallback_url : & str ) -> Option < String > {
2024-05-10 12:01:48 +02:00
let ctx_path = if context . is_empty () { String ::new () } else { format! ( " {context} /" ) };
2024-04-01 11:59:54 +02:00
if let Some ( user_info ) = input . get_user_info () {
Some ( format! ( " {} / {}{} / {} / {} " ,
& user_info . base_url ,
ctx_path ,
& user_info . username ,
& user_info . password ,
action_path
2023-12-01 21:38:23 +01:00
))
2024-11-10 14:17:24 +01:00
} else if ! fallback_url . is_empty () {
Some ( String ::from ( fallback_url ))
2024-04-01 11:59:54 +02:00
} else {
None
2023-12-01 21:38:23 +01:00
}
}
2024-05-03 01:47:41 +02:00
fn get_user_info ( user : & ProxyUserCredentials , cfg : & Config ) -> XtreamAuthorizationResponse {
2024-12-30 12:40:22 +01:00
let server_info = cfg . get_user_server_info ( user );
2024-11-01 11:14:04 +01:00
XtreamAuthorizationResponse ::new ( & server_info , user )
2023-10-09 00:17:54 +02:00
}
2024-12-30 16:39:41 +01:00
fn xtream_api_request_separate_number_and_remainder ( input : & str ) -> ( String , Option < String > ) {
2024-12-31 12:25:48 +01:00
input . rfind ( '.' ). map_or_else ( || ( input . to_string (), None ), | dot_index | {
2024-03-19 10:25:03 +01:00
let number_part = input [ .. dot_index ]. to_string ();
let rest = input [ dot_index .. ]. to_string ();
2024-12-30 16:39:41 +01:00
( number_part , if rest . len () < 2 { None } else { Some ( rest ) })
2024-11-04 18:46:56 +01:00
})
2024-03-19 10:20:06 +01:00
}
2023-11-22 21:06:58 +01:00
async fn xtream_player_api_stream (
2023-12-01 21:38:23 +01:00
req : & HttpRequest ,
2023-11-22 21:06:58 +01:00
api_req : & web ::Query < UserApiRequest > ,
2024-05-05 19:15:54 +02:00
app_state : & web ::Data < AppState > ,
2024-05-06 10:22:35 +02:00
stream_req : XtreamApiStreamRequest < '_ > ,
2023-11-22 21:06:58 +01:00
) -> HttpResponse {
2024-10-29 17:43:04 +01:00
let ( user , target ) = try_option_bad_request! ( get_user_target_by_credentials ( stream_req . username , stream_req . password , api_req , app_state ), false , format! ( "Could not find any user {} " , stream_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 ();
}
2024-12-30 16:39:41 +01:00
let ( action_stream_id , stream_ext ) = xtream_api_request_separate_number_and_remainder ( stream_req . stream_id );
2024-10-29 17:43:04 +01:00
let virtual_id : u32 = try_result_bad_request! ( action_stream_id . trim (). parse ());
2024-12-05 19:54:34 +01:00
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 ));
2024-10-29 17:43:04 +01:00
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} " , stream_req . context ));
2024-12-02 17:24:06 +01:00
if pli . item_type == PlaylistItemType ::LiveHls {
let stream_url = pli . url . to_string ();
2024-12-09 19:26:39 +01:00
debug_if_enabled! ( "Redirecting stream request to {}" , mask_sensitive_info ( & stream_url ));
2024-12-02 17:24:06 +01:00
return HttpResponse ::Found (). insert_header (( "Location" , stream_url )). finish ();
2024-11-10 14:17:24 +01:00
}
2024-10-29 17:43:04 +01:00
if user . proxy == ProxyType ::Redirect {
2024-12-09 19:26:39 +01:00
debug_if_enabled! ( "Redirecting stream request to {}" , mask_sensitive_info ( & pli . url ));
2024-12-03 14:49:23 +01:00
return HttpResponse ::Found (). insert_header (( "Location" , mask_sensitive_info ( pli . url . as_str ()))). finish ();
2023-11-22 21:06:58 +01:00
}
2024-10-29 17:43:04 +01:00
2024-12-30 16:39:41 +01:00
let extension = stream_ext . unwrap_or_else (
2024-12-31 12:31:40 +01:00
|| extract_extension_from_url ( & pli . url ). map_or_else ( String ::new , std ::string ::ToString ::to_string ));
2024-12-30 16:39:41 +01:00
let query_path = if stream_req . action_path . is_empty () {
format! ( " {}{extension} " , pli . provider_id )
} else {
format! ( " {} / {}{extension} " , stream_req . action_path , pli . provider_id )
};
let stream_url = try_option_bad_request! ( get_xtream_player_api_stream_url ( input ,
stream_req . context . to_string (). as_str (), & query_path , pli . url . as_str ()),
true , format! ( "Cant find stream url for target {target_name} , context {} , stream_id {virtual_id} " ,
stream_req . context ));
2024-12-09 19:26:39 +01:00
debug_if_enabled! ( "Streaming stream request from {}" , mask_sensitive_info ( & stream_url ));
2024-12-09 16:43:19 +01:00
let share_live_streams = is_stream_share_enabled ( pli . item_type , target );
2024-12-06 17:05:53 +01:00
stream_response ( app_state , & stream_url , req , Some ( input ), share_live_streams ). await
2023-11-22 21:06:58 +01:00
}
2025-01-05 18:22:23 +01:00
fn get_episode_id_and_field_name ( input : & str ) -> Option < ( u32 , & str ) > {
if let Some ( pos ) = input . find ( '_' ) {
let ( number_part , rest ) = input . split_at ( pos );
let field = & rest [ 1 .. ]; // cut _
if let Ok ( number ) = number_part . parse ::< u32 > () {
return Some (( number , field ));
}
}
None
}
async fn xtream_get_info_resource_url ( config : & Config , pli : & XtreamPlaylistItem , target : & ConfigTarget , resource : & str ) -> Result < Option < Rc < String >> , serde_json ::Error > {
let info_content = match pli . xtream_cluster {
XtreamCluster ::Video => {
xtream_repository ::xtream_load_vod_info ( config , target . name . as_str (), pli . get_virtual_id ()). await
}
XtreamCluster ::Series => {
xtream_repository ::xtream_load_series_info ( config , target . name . as_str (), pli . get_virtual_id ()). await
}
XtreamCluster ::Live => None ,
};
if let Some ( content ) = info_content {
let doc : Map < String , Value > = serde_json ::from_str ( & content ) ? ;
let ( field , possible_episode_id ) = if let Some ( field_name_with_episode_id ) = resource . strip_prefix ( INFO_RESOURCE_PREFIX_EPISODE ) {
if let Some (( episode_id , field_name )) = get_episode_id_and_field_name ( field_name_with_episode_id ) {
( field_name , Some ( episode_id ))
} else {
return Ok ( None );
}
} else {
( & resource [ INFO_RESOURCE_PREFIX . len () .. ], None )
};
let info_doc = match pli . xtream_cluster {
XtreamCluster ::Video | XtreamCluster ::Series => {
if let Some ( episode_id ) = possible_episode_id {
get_episode_info_doc ( & doc , episode_id )
} else {
doc . get ( TAG_INFO_DATA )
}
}
XtreamCluster ::Live => None ,
};
if let Some ( Value ::Object ( info_data )) = info_doc {
if field . starts_with ( PROP_BACKDROP_PATH ) {
return Ok ( get_backdrop_path_value ( field , info_data . get ( PROP_BACKDROP_PATH )));
} else if let Some ( Value ::String ( url )) = info_data . get ( field ) {
return Ok ( Some ( Rc ::new ( url . to_string ())));
}
}
}
Ok ( None )
}
fn get_episode_info_doc ( doc : & Map < String , Value > , episode_id : u32 ) -> Option <& Value > {
let episodes = doc . get ( TAG_EPISODES ) ? . as_object () ? ;
for season_episodes in episodes . values () {
if let Value ::Array ( episode_list ) = season_episodes {
for episode in episode_list {
if let Value ::Object ( episode_doc ) = episode {
if let Some ( episode_id_value ) = episode_doc . get ( TAG_ID ) {
if let Some ( doc_episode_id ) = get_u32_from_serde_value ( episode_id_value ) {
if doc_episode_id == episode_id {
return episode_doc . get ( TAG_INFO_DATA );
}
}
}
}
}
}
}
None
}
2025-01-03 16:49:24 +01:00
async fn xtream_player_api_resource (
req : & HttpRequest ,
api_req : & web ::Query < UserApiRequest > ,
app_state : & web ::Data < AppState > ,
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 ());
2025-01-05 18:22:23 +01:00
let resource = resource_req . action_path . trim ();
2025-01-03 16:49:24 +01:00
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 ));
2025-01-05 18:22:23 +01:00
let stream_url = if resource . starts_with ( INFO_RESOURCE_PREFIX ) {
try_result_bad_request! ( xtream_get_info_resource_url ( & app_state . config , & pli , target , resource ). await )
} else {
pli . get_field ( resource )
};
2025-01-03 16:49:24 +01:00
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
}
}
}
}
2024-12-09 16:43:19 +01:00
macro_rules ! create_xtream_player_api_stream {
( $fn_name :ident , $context :expr ) => {
async fn $fn_name (
req : HttpRequest ,
api_req : web ::Query < UserApiRequest > ,
path : web ::Path < ( String , String , String ) > ,
app_state : web ::Data < AppState > ,
) -> HttpResponse {
let ( username , password , stream_id ) = path . into_inner ();
xtream_player_api_stream ( & req , & api_req , & app_state , XtreamApiStreamRequest ::from ( $context , & username , & password , & stream_id , "" )). await
}
}
2023-12-02 20:47:26 +01:00
}
2025-01-05 18:22:23 +01:00
macro_rules ! create_xtream_player_api_resource {
2025-01-03 16:49:24 +01:00
( $fn_name :ident , $context :expr ) => {
async fn $fn_name (
req : HttpRequest ,
api_req : web ::Query < UserApiRequest > ,
path : web ::Path < ( String , String , String , String ) > ,
app_state : web ::Data < AppState > ,
) -> 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
}
}
}
2024-12-09 16:43:19 +01:00
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 );
2023-11-22 21:06:58 +01:00
2025-01-03 16:49:24 +01:00
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 );
2024-12-09 16:43:19 +01:00
fn get_non_empty < 'a > ( first : & 'a str , second : & 'a str , third : & 'a str ) -> & 'a str {
if ! first . is_empty () {
first
} else if ! second . is_empty () {
second
} else {
third
}
2023-11-22 21:06:58 +01:00
}
2023-11-24 22:53:33 +01:00
async fn xtream_player_api_timeshift_stream (
2023-12-01 21:38:23 +01:00
req : HttpRequest ,
2024-12-09 16:43:19 +01:00
api_query_req : web ::Query < UserApiRequest > ,
api_form_req : web ::Form < UserApiRequest > ,
2023-11-24 22:53:33 +01:00
path : web ::Path < ( String , String , String , String , String ) > ,
2024-05-05 19:15:54 +02:00
app_state : web ::Data < AppState > ,
2023-11-24 22:53:33 +01:00
) -> HttpResponse {
2024-12-09 16:43:19 +01:00
let ( path_username , path_password , path_duration , path_start , path_stream_id ) = path . into_inner ();
let username = get_non_empty ( & path_username , & api_query_req . username , & api_form_req . username );
let password = get_non_empty ( & path_password , & api_query_req . password , & api_form_req . password );
let stream_id = get_non_empty ( & path_stream_id , & api_query_req . stream , & api_form_req . stream );
let duration = get_non_empty ( & path_duration , & api_query_req . duration , & api_form_req . duration );
let start = get_non_empty ( & path_start , & api_query_req . start , & api_form_req . start );
2024-09-03 11:58:11 +02:00
let action_path = format! ( " {duration} / {start} " );
2024-12-09 16:43:19 +01:00
xtream_player_api_stream ( & req , & api_query_req , & app_state , XtreamApiStreamRequest ::from ( XtreamApiStreamContext ::Timeshift , username , password , stream_id , & action_path )). await
2024-09-03 11:58:11 +02:00
}
2024-05-03 01:47:41 +02:00
async fn xtream_get_stream_info_response ( app_state : & AppState , user : & ProxyUserCredentials ,
2024-04-01 11:59:54 +02:00
target : & ConfigTarget , stream_id : & str ,
2024-09-10 20:05:29 +02:00
cluster : XtreamCluster ) -> HttpResponse {
2024-10-25 18:26:25 +02:00
let virtual_id : u32 = match FromStr ::from_str ( stream_id ) {
2024-03-19 10:20:06 +01:00
Ok ( id ) => id ,
Err ( _ ) => return HttpResponse ::BadRequest (). finish ()
};
2023-12-04 16:19:24 +01:00
2024-12-05 19:54:34 +01:00
if let Ok ( pli ) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , Some ( cluster )). await {
2024-05-06 18:47:56 +02:00
let input_id = pli . input_id ;
2024-05-10 12:01:48 +02:00
if let Some ( input ) = app_state . config . get_input_by_id ( input_id ) {
2024-12-10 23:16:04 +01:00
if let Some ( info_url ) = download ::get_xtream_player_api_info_url ( input , cluster , pli . provider_id ) {
2024-05-07 17:13:22 +02:00
// Redirect is only possible for live streams, vod and series info needs to be modified
2024-09-10 20:05:29 +02:00
if user . proxy == ProxyType ::Redirect && cluster == XtreamCluster ::Live {
2024-04-01 11:59:54 +02:00
return HttpResponse ::Found (). insert_header (( "Location" , info_url )). finish ();
2025-01-05 18:22:23 +01:00
} else if let Ok ( content ) = download ::get_xtream_stream_info ( & app_state . config , user , input , target , & pli , info_url . as_str (), cluster ). await {
2024-05-06 10:22:35 +02:00
return HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( content );
2024-04-01 11:59:54 +02:00
}
2023-12-04 16:19:24 +01:00
}
}
}
2024-05-07 17:13:22 +02:00
match cluster {
XtreamCluster ::Live => HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( "{}" ),
XtreamCluster ::Video => HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( "{info:[]}" ),
XtreamCluster ::Series => HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( "[]" ),
}
2023-12-04 16:19:24 +01:00
}
2023-12-01 21:38:23 +01:00
2024-05-06 10:22:35 +02:00
async fn xtream_get_short_epg ( app_state : & AppState , user : & ProxyUserCredentials , target : & ConfigTarget , stream_id : & str , limit : & str ) -> HttpResponse {
let target_name = & target . name ;
if target . has_output ( & TargetType ::Xtream ) {
2024-10-28 20:15:27 +01:00
let virtual_id : u32 = match FromStr ::from_str ( stream_id . trim ()) {
2024-05-06 10:22:35 +02:00
Ok ( id ) => id ,
Err ( _ ) => return HttpResponse ::BadRequest (). finish ()
};
2024-03-18 17:30:52 +01:00
2024-12-05 19:54:34 +01:00
if let Ok ( pli ) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , None ). await {
2024-05-06 18:47:56 +02:00
let input_id : u16 = pli . input_id ;
2024-05-10 12:01:48 +02:00
if let Some ( input ) = app_state . config . get_input_by_id ( input_id ) {
2024-12-10 23:16:04 +01:00
if let Some ( action_url ) = download ::get_xtream_player_api_action_url ( input , ACTION_GET_SHORT_EPG ) {
2024-11-01 11:14:04 +01:00
let mut info_url = format! ( " {action_url} & {TAG_STREAM_ID} = {} " , pli . provider_id );
2024-05-06 10:22:35 +02:00
if ! ( limit . is_empty () || limit . eq ( "0" )) {
2024-05-10 12:01:48 +02:00
info_url = format! ( " {info_url} &limit= {limit} " );
2024-05-06 10:22:35 +02:00
}
2024-05-27 09:51:35 +02:00
if user . proxy == ProxyType ::Redirect {
return HttpResponse ::Found (). insert_header (( "Location" , info_url )). finish ();
}
2024-05-06 10:22:35 +02:00
2024-05-27 09:51:35 +02:00
return match request_utils ::download_text_content ( input , info_url . as_str (), None ). await {
Ok ( content ) => HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( content ),
Err ( err ) => {
2024-11-10 14:17:24 +01:00
error! ( "Failed to download epg {}" , mask_sensitive_info ( err . to_string (). as_str ()));
2024-05-27 09:51:35 +02:00
HttpResponse ::NoContent (). finish ()
2024-05-06 10:22:35 +02:00
}
2024-05-27 09:51:35 +02:00
};
2024-05-06 10:22:35 +02:00
}
}
}
}
2024-12-09 09:54:01 +01:00
warn! ( "Cant find short epg with id: {target_name}/{stream_id}" );
2023-12-02 20:47:26 +01:00
HttpResponse ::NoContent (). finish ()
2023-12-01 21:38:23 +01:00
}
2024-05-06 10:22:35 +02:00
async fn xtream_player_api_handle_content_action ( config : & Config , target_name : & str , action : & str , category_id : & str , req : & HttpRequest ) -> Option < HttpResponse > {
if let Ok (( path , content )) = match action {
2024-11-01 11:14:04 +01:00
ACTION_GET_LIVE_CATEGORIES => xtream_repository ::xtream_get_collection_path ( config , target_name , xtream_repository ::COL_CAT_LIVE ),
ACTION_GET_VOD_CATEGORIES => xtream_repository ::xtream_get_collection_path ( config , target_name , xtream_repository ::COL_CAT_VOD ),
ACTION_GET_SERIES_CATEGORIES => xtream_repository ::xtream_get_collection_path ( config , target_name , xtream_repository ::COL_CAT_SERIES ),
2024-11-10 14:17:24 +01:00
_ => Err ( Error ::new ( ErrorKind ::Other , "" ))
2024-05-06 10:22:35 +02:00
} {
if let Some ( file_path ) = path {
let category_id = category_id . trim ();
if ! category_id . is_empty () {
2024-11-01 11:14:04 +01:00
return Some ( serve_query ( & file_path , & HashMap ::from ([( TAG_CATEGORY_ID , category_id )])));
2024-05-06 10:22:35 +02:00
}
2024-05-10 12:01:48 +02:00
return Some ( serve_file ( & file_path , req , mime ::APPLICATION_JSON ). await );
2024-05-06 10:22:35 +02:00
} else if let Some ( payload ) = content {
return Some ( HttpResponse ::Ok (). body ( payload ));
}
2024-05-10 10:13:50 +02:00
return Some ( HttpResponse ::NoContent (). finish ());
2024-05-06 10:22:35 +02:00
}
None
}
2024-05-23 20:43:36 +02:00
async fn xtream_get_catchup_response ( app_state : & AppState , target : & ConfigTarget , stream_id : & str , start : & str , end : & str ) -> HttpResponse {
2024-10-29 17:43:04 +01:00
let virtual_id : u32 = try_result_bad_request! ( FromStr ::from_str ( stream_id ));
2024-12-05 19:54:34 +01:00
let pli = try_result_bad_request! ( xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , Some ( XtreamCluster ::Live )). await );
2024-10-29 17:43:04 +01:00
let input = try_option_bad_request! ( app_state . config . get_input_by_id ( pli . input_id ));
2024-12-10 23:16:04 +01:00
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 ( info_url . as_str (), input ). await );
2024-10-29 17:43:04 +01:00
let mut doc : Map < String , Value > = try_result_bad_request! ( serde_json ::from_str ( & content ));
2024-11-01 11:14:04 +01:00
let epg_listings = try_option_bad_request! ( doc . get_mut ( TAG_EPG_LISTINGS ). and_then ( Value ::as_array_mut ));
2024-10-29 17:43:04 +01:00
let target_path = try_option_bad_request! ( get_target_storage_path ( & app_state . config , target . name . as_str ()));
let mut target_id_mapping = TargetIdMapping ::new ( & target_path );
for epg_list_item in epg_listings . iter_mut (). filter_map ( Value ::as_object_mut ) {
// TODO epg_id
2024-11-01 11:14:04 +01:00
if let Some ( catchup_provider_id ) = epg_list_item . get ( TAG_ID ). and_then ( Value ::as_str ). and_then ( | id | id . parse ::< u32 > (). ok ()) {
2024-10-29 17:43:04 +01:00
let uuid = hash_string ( & format! ( " {} / {} " , pli . url , catchup_provider_id ));
2024-10-30 11:14:55 +01:00
let virtual_id = target_id_mapping . insert_entry ( uuid , catchup_provider_id , PlaylistItemType ::Catchup , pli . provider_id );
2024-11-01 11:14:04 +01:00
epg_list_item . insert ( TAG_ID . to_string (), Value ::String ( virtual_id . to_string ()));
2024-05-23 20:43:36 +02:00
}
}
2024-10-29 17:43:04 +01:00
if let Err ( err ) = target_id_mapping . persist () {
error! ( "Failed to write catchup id mapping {err}" );
return HttpResponse ::BadRequest (). finish ();
}
2024-11-04 18:46:56 +01:00
serde_json ::to_string ( & doc ). map_or_else ( | _ | HttpResponse ::BadRequest (). finish (), | result | HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( result ))
2024-05-23 20:43:36 +02:00
}
2024-12-09 09:54:01 +01:00
macro_rules ! skip_response_if_flag_set {
( $flag :expr , $stmt :expr ) => {
if $flag {
return HttpResponse ::NoContent (). finish ();
}
return $stmt ;
};
}
macro_rules ! skip_flag_optional {
( $flag :expr , $stmt :expr ) => {
if $flag {
None
} else {
Some ( $stmt )
}
};
}
2023-11-02 20:03:22 +01:00
async fn xtream_player_api (
2023-12-02 20:47:26 +01:00
req : & HttpRequest ,
api_req : UserApiRequest ,
2024-05-10 12:01:48 +02:00
app_state : & web ::Data < AppState > ,
2023-10-09 00:17:54 +02:00
) -> HttpResponse {
2024-11-21 18:11:25 +01:00
let user_target = get_user_target ( & api_req , app_state );
2024-12-05 19:54:34 +01:00
if let Some (( user , target )) = user_target {
2024-11-02 08:53:20 +01:00
if ! target . has_output ( & TargetType ::Xtream ) {
return HttpResponse ::Ok (). json ( get_user_info ( & user , & app_state . config ));
}
2024-10-30 11:14:55 +01:00
let action = api_req . action . trim ();
2024-11-02 08:53:20 +01:00
if action . is_empty () {
return HttpResponse ::Ok (). json ( get_user_info ( & user , & app_state . config ));
}
2024-12-09 09:54:01 +01:00
// Process specific playlist actions
let ( skip_live , skip_vod , skip_series ) = if let Some ( inputs ) = app_state . config . get_inputs_for_target ( & target . name ) {
inputs . iter (). fold (( true , true , true ), | acc , i | {
let ( l , v , s ) = acc ;
i . options . as_ref (). map_or (( false , false , false ), | o | ( l && o . xtream_skip_live , v && o . xtream_skip_vod , s && o . xtream_skip_series ))
})
} else {
( false , false , false )
};
2024-11-02 08:53:20 +01:00
match action {
ACTION_GET_SERIES_INFO => {
2024-12-09 09:54:01 +01:00
skip_response_if_flag_set! ( skip_series , xtream_get_stream_info_response ( app_state , & user , target , api_req . series_id . trim (), XtreamCluster ::Series ). await );
2024-11-02 08:53:20 +01:00
}
ACTION_GET_VOD_INFO => {
2024-12-09 09:54:01 +01:00
skip_response_if_flag_set! ( skip_vod , xtream_get_stream_info_response ( app_state , & user , target , api_req . vod_id . trim (), XtreamCluster ::Video ). await );
2024-11-02 08:53:20 +01:00
}
ACTION_GET_EPG | ACTION_GET_SHORT_EPG => {
return xtream_get_short_epg (
2024-12-05 19:54:34 +01:00
app_state , & user , target , api_req . stream_id . trim (), api_req . limit . trim (),
2024-11-02 08:53:20 +01:00
). await ;
}
ACTION_GET_CATCHUP_TABLE => {
2024-12-09 09:54:01 +01:00
skip_response_if_flag_set! ( skip_live , xtream_get_catchup_response ( app_state , target , api_req . stream_id . trim (), api_req . start . trim (), api_req . end . trim ()). await );
2024-10-30 11:14:55 +01:00
}
2024-11-02 08:53:20 +01:00
_ => {}
}
2023-11-22 21:06:58 +01:00
2024-11-02 08:53:20 +01:00
// Handle general content actions
if let Some ( response ) = xtream_player_api_handle_content_action (
2024-12-05 19:54:34 +01:00
& app_state . config , & target . name , action , api_req . category_id . trim (), req ,
2024-11-02 08:53:20 +01:00
). await {
return response ;
}
let category_id = api_req . category_id . trim (). parse ::< u32 > (). unwrap_or ( 0 );
let result = match action {
2024-12-09 09:54:01 +01:00
ACTION_GET_LIVE_STREAMS =>
2025-01-03 16:49:24 +01:00
skip_flag_optional! ( skip_live , xtream_repository ::xtream_load_rewrite_playlist ( XtreamCluster ::Live , & app_state . config , target , category_id , & user ). await ),
2024-12-09 09:54:01 +01:00
ACTION_GET_VOD_STREAMS =>
2025-01-03 16:49:24 +01:00
skip_flag_optional! ( skip_vod , xtream_repository ::xtream_load_rewrite_playlist ( XtreamCluster ::Video , & app_state . config , target , category_id , & user ). await ),
2024-12-09 09:54:01 +01:00
ACTION_GET_SERIES =>
2025-01-03 16:49:24 +01:00
skip_flag_optional! ( skip_series , xtream_repository ::xtream_load_rewrite_playlist ( XtreamCluster ::Series , & app_state . config , target , category_id , & user ). await ),
2024-12-23 11:30:33 +01:00
_ => Some ( Err ( info_err! ( format! ( "Cant find action: {action} for target: {} " , & target . name ))
2024-11-02 08:53:20 +01:00
)),
};
match result {
2024-12-09 09:54:01 +01:00
Some ( result_iter ) => {
match result_iter {
Ok ( xtream_iter ) => {
// Convert the iterator into a stream of `Bytes`
let content_stream = xtream_create_content_stream ( xtream_iter );
HttpResponse ::Ok ()
. content_type ( mime ::APPLICATION_JSON )
. streaming ( content_stream )
}
Err ( err ) => {
error! ( "Failed response for xtream target: {} action: {} error: {}" , & target . name , action , err );
HttpResponse ::NoContent (). finish ()
}
}
2024-11-02 08:53:20 +01:00
}
2024-12-09 09:54:01 +01:00
None => {
2024-11-02 08:53:20 +01:00
HttpResponse ::NoContent (). finish ()
2023-10-09 00:17:54 +02:00
}
}
2024-10-30 11:14:55 +01:00
} else {
2024-11-21 18:11:25 +01:00
if user_target . is_none () {
debug! ( "Cant find user!" );
} else if api_req . action . is_empty () {
debug! ( "Paremeter action is empty!" );
} else {
debug! ( "Bad request!" );
}
2024-10-30 11:14:55 +01:00
HttpResponse ::BadRequest (). finish ()
2023-10-09 00:17:54 +02:00
}
2023-10-20 12:18:44 +02:00
}
2023-11-02 20:03:22 +01:00
2024-12-05 19:54:34 +01:00
fn xtream_create_content_stream ( xtream_iter : impl Iterator < Item = String > ) -> impl Stream < Item = Result < Bytes , String >> {
2024-11-02 08:53:20 +01:00
let mut first_item = true ;
2024-12-09 09:54:01 +01:00
stream ::once ( async { Ok ::< Bytes , String > ( Bytes ::from ( "[" )) }). chain (
2024-12-09 16:43:19 +01:00
stream ::iter ( xtream_iter . map ( move | line | {
let line = if first_item {
first_item = false ;
line
} else {
format! ( ", {line} " )
};
Ok ::< Bytes , String > ( Bytes ::from ( line ))
2024-12-29 11:36:44 +01:00
})). chain ( stream ::once ( async { Ok ::< Bytes , String > ( Bytes ::from ( "]" )) })))
2024-11-02 08:53:20 +01:00
}
2023-12-02 20:47:26 +01:00
async fn xtream_player_api_get ( req : HttpRequest ,
api_req : web ::Query < UserApiRequest > ,
2024-05-05 19:15:54 +02:00
app_state : web ::Data < AppState > ,
2023-12-02 20:47:26 +01:00
) -> HttpResponse {
2024-05-05 19:15:54 +02:00
xtream_player_api ( & req , api_req . into_inner (), & app_state ). await
2023-12-02 20:47:26 +01:00
}
async fn xtream_player_api_post ( req : HttpRequest ,
api_req : web ::Form < UserApiRequest > ,
2024-05-05 19:15:54 +02:00
app_state : web ::Data < AppState > ,
2023-12-02 20:47:26 +01:00
) -> HttpResponse {
2024-05-05 19:15:54 +02:00
xtream_player_api ( & req , api_req . into_inner (), & app_state ). await
2023-12-02 20:47:26 +01:00
}
2024-12-09 16:43:19 +01:00
macro_rules ! register_xtream_api {
( $cfg :expr , [ $($path :expr ), * ]) => {{
$(
$cfg . service ( web ::resource ( $path ). route ( web ::get (). to ( xtream_player_api_get )). route ( web ::post (). to ( xtream_player_api_post )));
) *
}};
}
macro_rules ! register_xtream_api_stream {
( $cfg :expr , [ $( ( $path :expr , $fn_name :ident )), * ]) => {{
$(
$cfg . service ( web ::resource ( format! ( " {} / {{ username }} / {{ password }} / {{ stream_id }} " , $path )). route ( web ::get (). to ( $fn_name )));
) *
}};
}
2025-01-03 16:49:24 +01:00
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 )));
) *
}};
}
2024-12-09 16:43:19 +01:00
macro_rules ! register_xtream_api_timeshift {
( $cfg :expr , [ $($path :expr ), * ]) => {{
$(
$cfg . service ( web ::resource ( $path ). route ( web ::get (). to ( xtream_player_api_timeshift_stream )). route ( web ::post (). to ( xtream_player_api_timeshift_stream )));
) *
}};
}
2024-11-04 18:46:56 +01:00
pub fn xtream_api_register ( cfg : & mut web ::ServiceConfig ) {
2024-12-09 16:43:19 +01:00
register_xtream_api! ( cfg , [ "/player_api.php" , "/panel_api.php" , "/xtream" ]);
register_xtream_api_stream! ( cfg , [
( "" , xtream_player_api_live_stream_alt ),
( "/live" , xtream_player_api_live_stream ),
( "/movie" , xtream_player_api_movie_stream ),
( "/series" , xtream_player_api_series_stream )]);
register_xtream_api_timeshift! ( cfg , [
"/timeshift/{username}/{password}/{duration}/{start}/{stream_id}" ,
2024-12-29 11:36:44 +01:00
"/timeshift.php" ,
"/streaming/timeshift.php" ]);
2025-01-03 16:49:24 +01:00
register_xtream_api_resource! ( cfg , [
( "live" , xtream_player_api_live_resource ),
( "movie" , xtream_player_api_movie_resource ),
( "series" , xtream_player_api_series_resource )]);
2024-05-02 15:22:23 +02:00
/* 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)));
cfg.service(web::resource("/play/{token}/{type}").route(web::get().to(xtream_player_api_play_stream)));
*/
2023-11-02 20:03:22 +01:00
}