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 ;
2023-11-22 21:06:58 +01:00
use std ::str ::FromStr ;
2024-05-06 10:22:35 +02:00
2024-05-02 15:22:23 +02:00
use actix_web ::{ HttpRequest , HttpResponse , web };
2024-11-01 11:14:04 +01:00
use bytes ::Bytes ;
2024-11-02 08:53:20 +01:00
use futures ::Stream ;
2024-11-01 19:38:08 +01:00
use futures ::stream ::{ self , StreamExt };
2024-12-03 14:49:23 +01:00
use log ::{ debug , error , log_enabled , Level };
2024-05-06 10:22:35 +02:00
use serde_json ::{ Map , Value };
2023-10-20 12:18:44 +02:00
2024-11-01 11:14:04 +01:00
use crate ::api ::api_model ::{ AppState , UserApiRequest , XtreamAuthorizationResponse };
2024-05-06 10:22:35 +02:00
use crate ::api ::api_utils ::{ get_user_server_info , get_user_target , get_user_target_by_credentials , serve_file , stream_response };
2024-11-01 11:14:04 +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-04-01 11:59:54 +02:00
use crate ::model ::config ::{ Config , ConfigInput , ConfigTarget };
2024-05-06 10:22:35 +02:00
use crate ::model ::config ::TargetType ;
2024-10-29 17:43:04 +01:00
use crate ::model ::playlist ::{ PlaylistItemType , XtreamCluster , XtreamPlaylistItem };
2024-05-06 10:22:35 +02:00
use crate ::model ::xtream ::XtreamMappingOptions ;
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 ;
2024-03-26 10:32:04 +01:00
use crate ::utils ::{ json_utils , request_utils };
2024-11-10 14:17:24 +01:00
use crate ::utils ::request_utils ::mask_sensitive_info ;
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" ;
const ACTION_GET_LIVE_INFO : & str = "get_live_info" ;
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" ;
const ACTION_GET_SERIES_CATEGORIES : & str = "get_series_categories" ;
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_DIRECT_SOURCE : & str = "direct_source" ;
const TAG_STREAM_ID : & str = "stream_id" ;
const TAG_MOVIE_DATA : & str = "movie_data" ;
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-05-10 10:13:50 +02:00
username : & 'a str ,
password : & 'a str ,
stream_id : & 'a str ,
action_path : & 'a str ) -> Self {
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
fn get_xtream_player_api_action_url ( input : & ConfigInput , action : & str ) -> Option < String > {
2024-04-01 11:59:54 +02:00
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
2023-12-01 21:38:23 +01:00
}
}
2024-09-10 20:05:29 +02:00
fn get_xtream_player_api_info_url ( input : & ConfigInput , cluster : XtreamCluster , stream_id : u32 ) -> Option < String > {
2023-12-02 20:47:26 +01:00
let ( action , stream_id_field ) = match cluster {
2024-11-01 11:14:04 +01:00
XtreamCluster ::Live => ( ACTION_GET_LIVE_INFO , "live_id" ),
XtreamCluster ::Video => ( ACTION_GET_VOD_INFO , "vod_id" ),
XtreamCluster ::Series => ( ACTION_GET_SERIES_INFO , "series_id" ),
2023-12-02 20:47:26 +01:00
};
2024-05-10 12:01:48 +02:00
get_xtream_player_api_action_url ( input , action ). map ( | action_url | format! ( " {action_url} & {stream_id_field} = {stream_id} " ))
2023-12-02 20:47:26 +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-04-01 16:49:12 +02:00
let server_info = get_user_server_info ( cfg , user );
2024-11-01 11:14:04 +01:00
XtreamAuthorizationResponse ::new ( & server_info , user )
2023-10-09 00:17:54 +02:00
}
2024-05-05 19:15:54 +02:00
fn xtream_api_request_separate_number_and_rest ( input : & str ) -> ( String , String ) {
2024-11-04 18:46:56 +01:00
input . find ( '.' ). map_or_else ( || ( input . to_string (), String ::new ()), | 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 ();
( number_part , 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 ();
}
let ( action_stream_id , stream_ext ) = xtream_api_request_separate_number_and_rest ( stream_req . stream_id );
let virtual_id : u32 = try_result_bad_request! ( action_stream_id . trim (). parse ());
2024-10-30 11:14:55 +01:00
let pli = try_result_bad_request! ( xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , None ), 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-03 14:49:23 +01:00
if log_enabled! ( Level ::Debug ) {
debug! ( "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
let query_path = if stream_req . action_path . is_empty () {
format! ( " {}{stream_ext} " , pli . provider_id )
2023-12-05 17:54:42 +01:00
} else {
2024-10-29 17:43:04 +01:00
format! ( " {} / {}{stream_ext} " , stream_req . action_path , pli . provider_id )
};
if user . proxy == ProxyType ::Redirect {
2024-12-03 14:49:23 +01:00
if log_enabled! ( Level ::Debug ) {
debug! ( "Redirecting stream request to {}" , mask_sensitive_info ( & pli . url ));
}
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-03 14:49:23 +01:00
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 ));
if log_enabled! ( Level ::Debug ) {
debug! ( "Streaming stream request from {}" , mask_sensitive_info ( & stream_url ));
}
2024-10-29 17:43:04 +01:00
stream_response ( & stream_url , req , Some ( input )). await
2023-11-22 21:06:58 +01:00
}
2024-10-29 17:43:04 +01:00
2023-11-22 21:06:58 +01:00
async fn xtream_player_api_live_stream (
2023-12-01 21:38:23 +01:00
req : HttpRequest ,
2023-11-22 21:06:58 +01:00
api_req : web ::Query < UserApiRequest > ,
path : web ::Path < ( String , String , String ) > ,
2024-05-05 19:15:54 +02:00
app_state : web ::Data < AppState > ,
2023-11-22 21:06:58 +01:00
) -> HttpResponse {
let ( username , password , stream_id ) = path . into_inner ();
2024-05-06 10:22:35 +02:00
xtream_player_api_stream ( & req , & api_req , & app_state , XtreamApiStreamRequest ::from ( XtreamApiStreamContext ::Live , & username , & password , & stream_id , "" )). await
2023-11-22 21:06:58 +01:00
}
2023-12-02 20:47:26 +01:00
async fn xtream_player_api_live_stream_alt (
req : HttpRequest ,
api_req : web ::Query < UserApiRequest > ,
path : web ::Path < ( String , String , String ) > ,
2024-05-05 19:15:54 +02:00
app_state : web ::Data < AppState > ,
2023-12-02 20:47:26 +01:00
) -> HttpResponse {
let ( username , password , stream_id ) = path . into_inner ();
2024-05-08 20:42:15 +02:00
xtream_player_api_stream ( & req , & api_req , & app_state , XtreamApiStreamRequest ::from ( XtreamApiStreamContext ::LiveAlt , & username , & password , & stream_id , "" )). await
2023-12-02 20:47:26 +01:00
}
2023-11-22 21:06:58 +01:00
async fn xtream_player_api_series_stream (
2023-12-01 21:38:23 +01:00
req : HttpRequest ,
2023-11-22 21:06:58 +01:00
api_req : web ::Query < UserApiRequest > ,
path : web ::Path < ( String , String , String ) > ,
2024-05-05 19:15:54 +02:00
app_state : web ::Data < AppState > ,
2023-11-22 21:06:58 +01:00
) -> HttpResponse {
let ( username , password , stream_id ) = path . into_inner ();
2024-05-08 20:42:15 +02:00
xtream_player_api_stream ( & req , & api_req , & app_state , XtreamApiStreamRequest ::from ( XtreamApiStreamContext ::Series , & username , & password , & stream_id , "" )). await
2023-11-22 21:06:58 +01:00
}
async fn xtream_player_api_movie_stream (
2023-12-01 21:38:23 +01:00
req : HttpRequest ,
2023-11-22 21:06:58 +01:00
api_req : web ::Query < UserApiRequest > ,
path : web ::Path < ( String , String , String ) > ,
2024-05-05 19:15:54 +02:00
app_state : web ::Data < AppState > ,
2023-11-22 21:06:58 +01:00
) -> HttpResponse {
let ( username , password , stream_id ) = path . into_inner ();
2024-05-08 20:42:15 +02:00
xtream_player_api_stream ( & req , & api_req , & app_state , XtreamApiStreamRequest ::from ( XtreamApiStreamContext ::Movie , & username , & password , & stream_id , "" )). await
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 ,
2023-11-24 22:53:33 +01:00
api_req : web ::Query < UserApiRequest > ,
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 {
let ( username , password , duration , start , stream_id ) = path . into_inner ();
2024-05-24 15:10:29 +02:00
let action_path = format! ( " {duration} / {start} " );
2024-05-08 20:42:15 +02:00
xtream_player_api_stream ( & req , & api_req , & app_state , XtreamApiStreamRequest ::from ( XtreamApiStreamContext ::Timeshift , & username , & password , & stream_id , & action_path )). await
2024-03-18 17:30:52 +01:00
}
2024-09-03 11:58:11 +02:00
async fn xtream_player_api_streaming_timeshift (
req : HttpRequest ,
api_req : web ::Query < UserApiRequest > ,
app_state : web ::Data < AppState > ,
) -> HttpResponse {
let username = & api_req . username ;
let password = & api_req . password ;
let stream_id = & api_req . stream ;
let duration = & api_req . duration ;
let start = & api_req . start ;
let action_path = format! ( " {duration} / {start} " );
xtream_player_api_stream ( & req , & api_req , & app_state , XtreamApiStreamRequest ::from ( XtreamApiStreamContext ::Timeshift , username , password , stream_id , & action_path )). await
}
2024-05-07 17:13:22 +02:00
fn get_xtream_vod_info ( target : & ConfigTarget , pli : & XtreamPlaylistItem , content : & str ) -> Result < String , Error > {
if let Ok ( mut doc ) = serde_json ::from_str ::< Map < String , Value >> ( content ) {
2024-11-01 11:14:04 +01:00
if let Some ( Value ::Object ( movie_data )) = doc . get_mut ( TAG_MOVIE_DATA ) {
2024-10-25 00:39:22 +02:00
let stream_id = pli . virtual_id ;
2024-05-07 17:13:22 +02:00
let category_id = pli . category_id ;
2024-11-01 11:14:04 +01:00
movie_data . insert ( TAG_STREAM_ID . to_string (), Value ::Number ( serde_json ::value ::Number ::from ( stream_id )));
movie_data . insert ( TAG_CATEGORY_ID . to_string (), Value ::Number ( serde_json ::value ::Number ::from ( category_id )));
2024-05-07 17:13:22 +02:00
let options = XtreamMappingOptions ::from_target_options ( target . options . as_ref ());
if options . skip_video_direct_source {
2024-11-01 11:14:04 +01:00
movie_data . insert ( TAG_DIRECT_SOURCE . to_string (), Value ::String ( String ::new ()));
2024-05-07 17:13:22 +02:00
} else {
2024-11-01 11:14:04 +01:00
movie_data . insert ( TAG_DIRECT_SOURCE . to_string (), Value ::String ( pli . url . to_string ()));
2024-05-07 17:13:22 +02:00
}
if let Ok ( result ) = serde_json ::to_string ( & doc ) {
return Ok ( result );
}
}
}
2024-10-25 00:39:22 +02:00
Err ( Error ::new ( ErrorKind ::Other , format! ( "Failed to get vod info for id {} " , pli . virtual_id )))
2024-05-07 17:13:22 +02:00
}
2024-05-23 20:43:36 +02:00
async fn xtream_get_stream_info_content ( info_url : & str , input : & ConfigInput ) -> Result < String , Error > {
2024-05-27 09:51:35 +02:00
request_utils ::download_text_content ( input , info_url , None ). await
2024-05-23 20:43:36 +02:00
}
async fn xtream_get_stream_info ( config : & Config , input : & ConfigInput , target : & ConfigTarget ,
2024-09-10 20:05:29 +02:00
pli : & XtreamPlaylistItem , info_url : & str , cluster : XtreamCluster ) -> Result < String , Error > {
if cluster == XtreamCluster ::Series {
2024-10-29 17:43:04 +01:00
if let Some ( content ) = xtream_repository ::xtream_load_series_info ( config , target . name . as_str (), pli . virtual_id ) {
2024-05-23 20:43:36 +02:00
return Ok ( content );
}
}
if let Ok ( content ) = xtream_get_stream_info_content ( info_url , input ). await {
return match cluster {
XtreamCluster ::Live => Ok ( content ),
2024-10-25 18:26:25 +02:00
XtreamCluster ::Video => get_xtream_vod_info ( target , pli , & content ),
2024-10-28 20:15:27 +01:00
XtreamCluster ::Series => xtream_repository ::write_and_get_xtream_series_info ( config , target , pli , & content ),
2024-05-23 20:43:36 +02:00
};
}
2024-11-10 14:17:24 +01:00
Err ( Error ::new ( ErrorKind ::Other , format! ( "Cant find stream with id: {} / {} / {} " ,
2024-10-25 00:39:22 +02:00
target . name . replace ( ' ' , "_" ). as_str (), & cluster , pli . virtual_id )))
2023-12-01 21:38:23 +01: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-10-25 18:26:25 +02:00
if let Ok ( pli ) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , Some ( cluster )) {
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-05-08 20:42:15 +02:00
if let Some ( info_url ) = 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 ();
2024-05-08 20:42:15 +02:00
} else if let Ok ( content ) = xtream_get_stream_info ( & app_state . config , 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-10-28 20:15:27 +01:00
if let Ok ( pli ) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , None ) {
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-11-01 11:14:04 +01:00
if let Some ( action_url ) = get_xtream_player_api_action_url ( input , ACTION_GET_SHORT_EPG ) {
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-11-01 11:14:04 +01:00
error! ( "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 ));
let pli = try_result_bad_request! ( xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , Some ( XtreamCluster ::Live )));
let input = try_option_bad_request! ( app_state . config . get_input_by_id ( pli . input_id ));
2024-11-01 11:14:04 +01:00
let info_url = try_option_bad_request! ( 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 )));
2024-10-29 17:43:04 +01:00
let content = try_result_bad_request! ( xtream_get_stream_info_content ( info_url . as_str (), input ). await );
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
}
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 );
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 ));
}
match action {
ACTION_GET_SERIES_INFO => {
return xtream_get_stream_info_response (
app_state , & user , target , api_req . series_id . trim (), XtreamCluster ::Series
). await ;
}
ACTION_GET_VOD_INFO => {
return xtream_get_stream_info_response (
app_state , & user , target , api_req . vod_id . trim (), XtreamCluster ::Video
). await ;
}
ACTION_GET_EPG | ACTION_GET_SHORT_EPG => {
return xtream_get_short_epg (
app_state , & user , target , api_req . stream_id . trim (), api_req . limit . trim ()
). await ;
}
ACTION_GET_CATCHUP_TABLE => {
return 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 (
& app_state . config , & target . name , action , api_req . category_id . trim (), req
). await {
return response ;
}
// Process specific playlist actions
let category_id = api_req . category_id . trim (). parse ::< u32 > (). unwrap_or ( 0 );
let result = match action {
ACTION_GET_LIVE_STREAMS => xtream_repository ::xtream_load_rewrite_playlist (
XtreamCluster ::Live , & app_state . config , target , category_id
),
ACTION_GET_VOD_STREAMS => xtream_repository ::xtream_load_rewrite_playlist (
XtreamCluster ::Video , & app_state . config , target , category_id
),
ACTION_GET_SERIES => xtream_repository ::xtream_load_rewrite_playlist (
XtreamCluster ::Series , & app_state . config , target , category_id
),
_ => Err ( M3uFilterError ::new ( M3uFilterErrorKind ::Info , format! ( "Cant find action: {action} for target: {} " , & target . name )
)),
};
match result {
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 ()
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-11-02 08:53:20 +01:00
fn xtream_create_content_stream ( xtream_iter : impl Iterator < Item = String > ) -> impl Stream < Item = Result < Bytes , String >> {
let mut first_item = true ;
stream ::iter ( xtream_iter . map ( move | line | {
let line = if first_item {
first_item = false ;
format! ( "[ {line} " )
} else {
format! ( ", {line} " )
};
Ok ::< Bytes , String > ( Bytes ::from ( line ))
}))
. chain ( stream ::once ( async { Ok ::< Bytes , String > ( Bytes ::from ( "]" )) }))
}
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-11-04 18:46:56 +01:00
pub fn xtream_api_register ( cfg : & mut web ::ServiceConfig ) {
2024-11-21 18:11:25 +01:00
cfg . service ( web ::resource ( "/player_api.php" ). route ( web ::get (). to ( xtream_player_api_get )). route ( web ::post (). to ( xtream_player_api_post )))
. service ( web ::resource ( "/panel_api.php" ). route ( web ::get (). to ( xtream_player_api_get )). route ( web ::post (). to ( xtream_player_api_post )))
2024-05-06 10:22:35 +02:00
. service ( web ::resource ( "/xtream" ). route ( web ::get (). to ( xtream_player_api_get )). route ( web ::post (). to ( xtream_player_api_post )))
. service ( web ::resource ( "/{username}/{password}/{stream_id}" ). route ( web ::get (). to ( xtream_player_api_live_stream_alt )))
. service ( web ::resource ( "/live/{username}/{password}/{stream_id}" ). route ( web ::get (). to ( xtream_player_api_live_stream )))
. service ( web ::resource ( "/movie/{username}/{password}/{stream_id}" ). route ( web ::get (). to ( xtream_player_api_movie_stream )))
. service ( web ::resource ( "/series/{username}/{password}/{stream_id}" ). route ( web ::get (). to ( xtream_player_api_series_stream )))
2024-09-03 11:58:11 +02:00
. service ( web ::resource ( "/timeshift/{username}/{password}/{duration}/{start}/{stream_id}" ). route ( web ::get (). to ( xtream_player_api_timeshift_stream )))
. service ( web ::resource ( "/timeshift.php" ). route ( web ::get (). to ( xtream_player_api_streaming_timeshift )))
. service ( web ::resource ( "/streaming/timeshift.php" ). route ( web ::get (). to ( xtream_player_api_streaming_timeshift )));
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
}