2023-10-09 00:17:54 +02:00
// https://github.com/tellytv/go.xtream-codes/blob/master/structs.go
2025-03-11 14:56:10 +01:00
use crate ::api ::api_utils ::{ redirect , try_option_bad_request , try_result_bad_request };
2025-02-07 13:15:39 +01:00
use crate ::utils ::trace_if_enabled ;
2025-02-22 17:11:16 +01:00
use std ::collections ::{ HashMap , HashSet };
2024-05-05 19:15:54 +02:00
use std ::fmt ::{ Display , Formatter };
2024-03-26 10:26:12 +01:00
use std ::path ::Path ;
2023-11-22 21:06:58 +01:00
use std ::str ::FromStr ;
2025-02-05 20:01:49 +01:00
use std ::sync ::Arc ;
2025-03-11 14:56:10 +01:00
use axum ::http ::{ HeaderMap , StatusCode };
use axum ::response ::IntoResponse ;
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 };
2025-03-11 14:56:10 +01:00
use serde ::{ Deserialize , Serialize };
2024-05-06 10:22:35 +02:00
use serde_json ::{ Map , Value };
2025-02-07 13:15:39 +01:00
use crate ::api ::api_utils ;
2025-01-31 23:08:23 +01:00
use crate ::api ::api_utils ::{ get_user_target , get_user_target_by_credentials , resource_response , separate_number_and_remainder , serve_file , stream_response };
2025-02-05 20:01:49 +01:00
use crate ::api ::endpoints ::hls_api ::handle_hls_stream_request ;
2025-02-07 13:15:39 +01:00
use crate ::api ::endpoints ::xmltv_api ::get_empty_epg_response ;
2024-12-05 19:54:34 +01:00
use crate ::api ::model ::app_state ::AppState ;
use crate ::api ::model ::request ::UserApiRequest ;
2025-03-16 17:08:00 +01:00
use crate ::api ::model ::streams ::provider_stream ::{ create_custom_video_stream_response , CustomVideoStreamType };
2024-12-05 19:54:34 +01:00
use crate ::api ::model ::xtream ::XtreamAuthorizationResponse ;
2025-01-17 18:32:17 +01:00
use crate ::m3u_filter_error ::{ str_to_io_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 };
2025-01-16 17:32:17 +01:00
use crate ::model ::xtream ::{ INFO_RESOURCE_PREFIX , INFO_RESOURCE_PREFIX_EPISODE , PROP_BACKDROP_PATH , SEASON_RESOURCE_PREFIX };
2025-03-16 17:08:00 +01:00
use crate ::repository ::playlist_repository ::{ get_target_id_mapping };
2025-01-29 17:33:06 +01:00
use crate ::repository ::storage ::{ get_target_storage_path , hex_encode };
2025-02-22 17:11:16 +01:00
use crate ::repository ::{ user_repository , xtream_repository };
2025-01-30 17:23:41 +01:00
use crate ::repository ::xtream_repository ::{ TAG_EPISODES , TAG_INFO_DATA , TAG_SEASONS_DATA };
2025-02-05 20:01:49 +01:00
use crate ::utils ::hash_utils ::generate_playlist_uuid ;
use crate ::utils ::json_utils ::get_u32_from_serde_value ;
2025-03-19 09:54:59 +01:00
use crate ::utils ::network ::request ::{ extract_extension_from_url , replace_url_extension , sanitize_sensitive_info , DASH_EXT , HLS_EXT };
2025-02-05 20:01:49 +01:00
use crate ::utils ::network ::xtream ::{ create_vod_info_from_item , ACTION_GET_LIVE_CATEGORIES , ACTION_GET_LIVE_STREAMS , ACTION_GET_SERIES , ACTION_GET_SERIES_CATEGORIES , ACTION_GET_SERIES_INFO , ACTION_GET_VOD_CATEGORIES , ACTION_GET_VOD_INFO , ACTION_GET_VOD_STREAMS };
use crate ::utils ::json_utils ;
use crate ::utils ::debug_if_enabled ;
use crate ::m3u_filter_error ::info_err ;
use crate ::utils ::network ::{ request , xtream };
2024-12-10 23:16:04 +01:00
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 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" ;
2025-03-11 14:56:10 +01:00
#[derive(Serialize, Deserialize, Debug)]
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 : & '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 {
2025-01-30 12:32:09 +01:00
Self ::Live | Self ::LiveAlt => Self ::LIVE ,
2024-11-04 18:46:56 +01:00
Self ::Movie => Self ::MOVIE ,
Self ::Series => Self ::SERIES ,
Self ::Timeshift => Self ::TIMESHIFT ,
2024-05-10 10:13:50 +02:00
})
}
}
2025-03-11 14:56:10 +01:00
#[derive(Serialize, Deserialize, Debug)]
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 ,
}
}
}
2025-03-11 14:56:10 +01:00
pub fn serve_query ( file_path : & Path , filter : & HashMap <& str , HashSet < String >> ) -> impl axum ::response ::IntoResponse + Send {
2024-05-07 20:27:07 +02:00
let filtered = json_utils ::json_filter_file ( file_path , filter );
2025-03-11 14:56:10 +01:00
axum ::Json ( filtered )
2024-03-26 10:26:12 +01:00
}
2023-12-01 21:38:23 +01:00
2025-02-03 23:54:08 +01:00
fn get_xtream_player_api_stream_url ( input : & ConfigInput , context : & XtreamApiStreamContext , action_path : & str , fallback_url : & str ) -> Option < String > {
2025-03-19 09:54:59 +01:00
if let Some ( input_user_info ) = input . get_user_info () {
2025-02-03 23:54:08 +01:00
let ctx = match context {
XtreamApiStreamContext ::LiveAlt |
XtreamApiStreamContext ::Live => {
2025-02-22 17:11:16 +01:00
let use_prefix = input . options . as_ref (). is_none_or ( | o | o . xtream_live_stream_use_prefix );
2025-02-03 23:54:08 +01:00
String ::from ( if use_prefix { "live" } else { "" })
},
XtreamApiStreamContext ::Movie
| XtreamApiStreamContext ::Series
| XtreamApiStreamContext ::Timeshift => context . to_string ()
};
let ctx_path = if ctx . is_empty () { String ::new () } else { format! ( " {ctx} /" ) };
2024-04-01 11:59:54 +02:00
Some ( format! ( " {} / {}{} / {} / {} " ,
2025-03-19 09:54:59 +01:00
& input_user_info . base_url ,
2024-04-01 11:59:54 +02:00
ctx_path ,
2025-03-19 09:54:59 +01:00
& input_user_info . username ,
& input_user_info . password ,
2024-04-01 11:59:54 +02:00
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
}
}
2025-03-11 14:56:10 +01:00
async fn get_user_info ( user : & ProxyUserCredentials , app_state : & AppState ) -> XtreamAuthorizationResponse {
let server_info = app_state . config . get_user_server_info ( user ). await ;
let active_connections = app_state . get_active_connections_for_user ( & user . username ). await ;
2025-02-07 11:30:37 +01:00
XtreamAuthorizationResponse ::new ( & server_info , user , active_connections , app_state . config . user_access_control )
2023-10-09 00:17:54 +02:00
}
2023-11-22 21:06:58 +01:00
async fn xtream_player_api_stream (
2025-03-11 14:56:10 +01:00
req_headers : & HeaderMap ,
api_req : & UserApiRequest ,
app_state : & Arc < AppState > ,
2024-05-06 10:22:35 +02:00
stream_req : XtreamApiStreamRequest < '_ > ,
2025-03-11 14:56:10 +01:00
) -> impl axum ::response ::IntoResponse + Send {
2025-02-05 20:01:49 +01:00
let ( user , target ) = try_option_bad_request! ( get_user_target_by_credentials ( stream_req . username , stream_req . password , api_req , app_state ). await , false , format! ( "Could not find any user {} " , stream_req . username ));
2025-03-16 17:08:00 +01:00
if user . permission_denied ( app_state ) {
2025-03-11 14:56:10 +01:00
return axum ::http ::StatusCode ::FORBIDDEN . into_response ();
2025-02-06 20:31:51 +01:00
}
2025-03-16 17:08:00 +01:00
if user . connections_exhausted ( app_state ). await {
return create_custom_video_stream_response ( & app_state . config , & CustomVideoStreamType ::UserConnectionsExhausted ). into_response ();
}
2024-10-29 17:43:04 +01:00
let target_name = & target . name ;
if ! target . has_output ( & TargetType ::Xtream ) {
debug! ( "Target has no xtream output {}" , target_name );
2025-03-11 14:56:10 +01:00
return axum ::http ::StatusCode ::BAD_REQUEST . into_response ();
2024-10-29 17:43:04 +01:00
}
2025-01-31 23:08:23 +01:00
let ( action_stream_id , stream_ext ) = 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 ());
2025-02-06 10:49:34 +01:00
let ( pli , mapping ) = 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 ));
2025-01-27 15:58:57 +01:00
let input = try_option_bad_request! ( app_state . config . get_input_by_name ( pli . input_name . as_str ()), true , format! ( "Cant find input for target {target_name} , context {} , stream_id {virtual_id} " , stream_req . context ));
2024-10-29 17:43:04 +01:00
2025-03-14 13:15:26 +01:00
let is_hls_request = pli . item_type == PlaylistItemType ::LiveHls || stream_ext . as_deref () == Some ( HLS_EXT );
2025-01-31 23:08:23 +01:00
2024-10-29 17:43:04 +01:00
if user . proxy == ProxyType ::Redirect {
2025-01-26 18:20:04 +01:00
if pli . xtream_cluster == XtreamCluster ::Series {
2025-01-26 13:16:07 +01:00
let ext = stream_ext . unwrap_or_else ( String ::new );
let url = input . url . as_str ();
let username = input . username . as_ref (). map_or ( "" , | v | v );
let password = input . password . as_ref (). map_or ( "" , | v | v );
let stream_url = format! ( " {url} /series/ {username} / {password} / {}{ext} " , mapping . provider_id );
2025-03-11 14:56:10 +01:00
return redirect ( & stream_url ). into_response ();
2025-01-26 13:16:07 +01:00
}
2025-01-31 23:08:23 +01:00
2025-03-14 13:15:26 +01:00
// if pli.item_type == PlaylistItemType::LiveHls {
2025-03-19 09:54:59 +01:00
// let redirect_url = &replace_extension(&pli.url, HLS_EXT);
2025-03-14 13:15:26 +01:00
// debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(redirect_url));
// return redirect(redirect_url).into_response();
// }
2025-03-19 09:54:59 +01:00
if pli . item_type == PlaylistItemType ::LiveDash {
let redirect_url = & replace_url_extension ( & pli . url , DASH_EXT );
debug_if_enabled! ( "Redirecting stream request to {}" , sanitize_sensitive_info ( redirect_url ));
return redirect ( redirect_url ). into_response ();
}
2025-03-14 13:15:26 +01:00
debug_if_enabled! ( "Redirecting stream request to {}" , sanitize_sensitive_info ( & pli . url ));
return redirect ( & pli . url ). into_response ();
2025-01-31 23:08:23 +01:00
}
// Reverse proxy mode
if is_hls_request {
2025-03-14 13:15:26 +01:00
return handle_hls_stream_request ( app_state , & user , & pli . url , pli . virtual_id , input , TargetType ::Xtream ). await . into_response ();
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 ,
2025-02-03 23:54:08 +01:00
& stream_req . context , & query_path , pli . url . as_str ()),
2024-12-30 16:39:41 +01:00
true , format! ( "Cant find stream url for target {target_name} , context {} , stream_id {virtual_id} " ,
stream_req . context ));
2025-01-30 12:30:04 +01:00
2025-01-31 23:08:23 +01:00
trace_if_enabled! ( "Streaming stream request from {}" , sanitize_sensitive_info ( & stream_url ));
2025-03-11 14:56:10 +01:00
stream_response ( app_state , & stream_url , req_headers , Some ( input ), pli . item_type , target , & user ). await . into_response ()
2023-11-22 21:06:58 +01:00
}
2025-01-31 23:08:23 +01:00
2025-01-16 17:32:17 +01:00
fn get_doc_id_and_field_name ( input : & str ) -> Option < ( u32 , & str ) > {
2025-01-05 18:22:23 +01:00
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
}
2025-03-11 14:56:10 +01:00
fn get_doc_resource_field_value ( field : & str , doc : Option <& Value > ) -> Option < String > {
2025-01-16 17:32:17 +01:00
if let Some ( Value ::Object ( info_data )) = doc {
if field . starts_with ( PROP_BACKDROP_PATH ) {
return get_backdrop_path_value ( field , info_data . get ( PROP_BACKDROP_PATH ));
} else if let Some ( Value ::String ( url )) = info_data . get ( field ) {
2025-03-11 14:56:10 +01:00
return Some ( url . to_string ());
2025-01-16 17:32:17 +01:00
}
}
None
}
2025-03-11 14:56:10 +01:00
fn xtream_get_info_resource_url ( config : & Config , pli : & XtreamPlaylistItem , target : & ConfigTarget , resource : & str ) -> Result < Option < String > , serde_json ::Error > {
2025-01-05 18:22:23 +01:00
let info_content = match pli . xtream_cluster {
XtreamCluster ::Video => {
2025-02-06 10:49:34 +01:00
xtream_repository ::xtream_load_vod_info ( config , target . name . as_str (), pli . get_virtual_id ())
2025-01-05 18:22:23 +01:00
}
XtreamCluster ::Series => {
2025-02-06 10:49:34 +01:00
xtream_repository ::xtream_load_series_info ( config , target . name . as_str (), pli . get_virtual_id ())
2025-01-05 18:22:23 +01:00
}
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 ) {
2025-01-16 17:32:17 +01:00
if let Some (( episode_id , field_name )) = get_doc_id_and_field_name ( field_name_with_episode_id ) {
2025-01-05 18:22:23 +01:00
( 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 ,
};
2025-01-16 17:32:17 +01:00
if let Some ( value ) = get_doc_resource_field_value ( field , info_doc ) {
return Ok ( Some ( value ));
2025-01-05 18:22:23 +01:00
}
}
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-16 17:32:17 +01:00
fn get_season_info_doc ( doc : & Vec < Value > , season_id : u32 ) -> Option <& Value > {
for season in doc {
if let Value ::Object ( season_doc ) = season {
if let Some ( season_id_value ) = season_doc . get ( TAG_ID ) {
if let Some ( doc_season_id ) = get_u32_from_serde_value ( season_id_value ) {
if doc_season_id == season_id {
return Some ( season );
}
}
}
}
}
None
}
2025-03-11 14:56:10 +01:00
fn xtream_get_season_resource_url ( config : & Config , pli : & XtreamPlaylistItem , target : & ConfigTarget , resource : & str ) -> Result < Option < String > , serde_json ::Error > {
2025-01-16 17:32:17 +01:00
let info_content = match pli . xtream_cluster {
XtreamCluster ::Series => {
2025-02-06 10:49:34 +01:00
xtream_repository ::xtream_load_series_info ( config , target . name . as_str (), pli . get_virtual_id ())
2025-01-16 17:32:17 +01:00
}
XtreamCluster ::Video | XtreamCluster ::Live => None ,
};
if let Some ( content ) = info_content {
let doc : Map < String , Value > = serde_json ::from_str ( & content ) ? ;
if let Some ( field_name_with_season_id ) = resource . strip_prefix ( SEASON_RESOURCE_PREFIX ) {
if let Some (( season_id , field )) = get_doc_id_and_field_name ( field_name_with_season_id ) {
let seasons_doc = match pli . xtream_cluster {
XtreamCluster ::Series => doc . get ( TAG_SEASONS_DATA ),
XtreamCluster ::Video | XtreamCluster ::Live => None ,
};
if let Some ( Value ::Array ( seasons )) = seasons_doc {
if let Some ( value ) = get_doc_resource_field_value ( field , get_season_info_doc ( seasons , season_id )) {
return Ok ( Some ( value ));
}
}
}
}
}
Ok ( None )
}
2025-01-03 16:49:24 +01:00
async fn xtream_player_api_resource (
2025-03-11 14:56:10 +01:00
req_headers : & HeaderMap ,
api_req : & UserApiRequest ,
app_state : & Arc < AppState > ,
2025-01-03 16:49:24 +01:00
resource_req : XtreamApiStreamRequest < '_ > ,
2025-03-11 14:56:10 +01:00
) -> impl axum ::response ::IntoResponse {
2025-02-05 20:01:49 +01:00
let ( user , target ) = try_option_bad_request! ( get_user_target_by_credentials ( resource_req . username , resource_req . password , api_req , app_state ). await , false , format! ( "Could not find any user {} " , resource_req . username ));
2025-03-16 17:08:00 +01:00
if user . permission_denied ( app_state ) {
2025-03-11 14:56:10 +01:00
return axum ::http ::StatusCode ::FORBIDDEN . into_response ();
2025-02-06 20:31:51 +01:00
}
2025-01-03 16:49:24 +01:00
let target_name = & target . name ;
if ! target . has_output ( & TargetType ::Xtream ) {
debug! ( "Target has no xtream output {}" , target_name );
2025-03-11 14:56:10 +01:00
return axum ::http ::StatusCode ::BAD_REQUEST . into_response ();
2025-01-03 16:49:24 +01:00
}
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-02-06 10:49:34 +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 ));
2025-01-05 18:22:23 +01:00
let stream_url = if resource . starts_with ( INFO_RESOURCE_PREFIX ) {
2025-02-06 10:49:34 +01:00
try_result_bad_request! ( xtream_get_info_resource_url ( & app_state . config , & pli , target , resource ))
2025-01-16 17:32:17 +01:00
} else if resource . starts_with ( SEASON_RESOURCE_PREFIX ) {
2025-02-06 10:49:34 +01:00
try_result_bad_request! ( xtream_get_season_resource_url ( & app_state . config , & pli , target , resource ))
2025-01-05 18:22:23 +01:00
} else {
pli . get_field ( resource )
};
2025-01-03 16:49:24 +01:00
match stream_url {
2025-03-11 14:56:10 +01:00
None => axum ::http ::StatusCode ::NOT_FOUND . into_response (),
2025-01-03 16:49:24 +01:00
Some ( url ) => {
if user . proxy == ProxyType ::Redirect {
2025-01-31 23:08:23 +01:00
trace_if_enabled! ( "Redirecting resource request to {}" , sanitize_sensitive_info ( & url ));
2025-03-11 14:56:10 +01:00
redirect ( url . as_str ()). into_response ()
2025-01-03 16:49:24 +01:00
} else {
2025-01-31 23:08:23 +01:00
trace_if_enabled! ( "Resource request to {}" , sanitize_sensitive_info ( & url ));
2025-03-11 14:56:10 +01:00
resource_response ( app_state , url . as_str (), req_headers , None ). await . into_response ()
2025-01-03 16:49:24 +01:00
}
}
}
}
2025-03-11 14:56:10 +01:00
macro_rules ! create_xtream_player_api_resource {
2024-12-09 16:43:19 +01:00
( $fn_name :ident , $context :expr ) => {
async fn $fn_name (
2025-03-11 14:56:10 +01:00
axum ::extract ::Path (( username , password , stream_id , resource )) : axum ::extract ::Path < ( String , String , String , String ) > ,
axum ::extract ::State ( app_state ) : axum ::extract ::State < Arc < AppState >> ,
axum ::extract ::Query ( api_req ) : axum ::extract ::Query < UserApiRequest > ,
req_headers : HeaderMap ,
) -> impl axum ::response ::IntoResponse {
xtream_player_api_resource ( & req_headers , & api_req , & app_state , XtreamApiStreamRequest ::from ( $context , & username , & password , & stream_id , & resource )). await . into_response ()
2024-12-09 16:43:19 +01:00
}
}
2023-12-02 20:47:26 +01:00
}
2025-03-11 14:56:10 +01:00
macro_rules ! create_xtream_player_api_stream {
2025-01-03 16:49:24 +01:00
( $fn_name :ident , $context :expr ) => {
async fn $fn_name (
2025-03-11 14:56:10 +01:00
axum ::extract ::Path (( username , password , stream_id )) : axum ::extract ::Path < ( String , String , String ) > ,
axum ::extract ::State ( app_state ) : axum ::extract ::State < Arc < AppState >> ,
axum ::extract ::Query ( api_req ) : axum ::extract ::Query < UserApiRequest > ,
req_headers : HeaderMap ,
) -> impl axum ::response ::IntoResponse + Send {
xtream_player_api_stream ( & req_headers , & api_req , & app_state , XtreamApiStreamRequest ::from ( $context , & username , & password , & stream_id , "" )). await . into_response ()
2025-01-03 16:49:24 +01:00
}
}
}
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 (
2025-03-11 14:56:10 +01:00
req_headers : HeaderMap ,
axum ::extract ::Query ( api_query_req ) : axum ::extract ::Query < UserApiRequest > ,
axum ::extract ::Path (( path_username , path_password , path_duration , path_start , path_stream_id )) : axum ::extract ::Path < ( String , String , String , String , String ) > ,
axum ::extract ::State ( app_state ) : axum ::extract ::State < Arc < AppState >> ,
axum ::extract ::Form ( api_form_req ) : axum ::extract ::Form < UserApiRequest > ,
) -> impl axum ::response ::IntoResponse + Send {
2024-12-09 16:43:19 +01:00
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} " );
2025-03-11 14:56:10 +01:00
xtream_player_api_stream ( & req_headers , & 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 ,
2025-03-11 14:56:10 +01:00
cluster : XtreamCluster ) -> impl axum ::response ::IntoResponse + Send {
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 ,
2025-03-11 14:56:10 +01:00
Err ( _ ) => return axum ::http ::StatusCode ::BAD_REQUEST . into_response ()
2024-03-19 10:20:06 +01:00
};
2023-12-04 16:19:24 +01:00
2025-02-06 10:49:34 +01:00
if let Ok (( pli , virtual_record )) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , Some ( cluster )) {
2025-01-31 23:08:23 +01:00
if pli . provider_id > 0 {
2025-03-11 14:56:10 +01:00
let input_name = & pli . input_name ;
2025-01-31 23:08:23 +01:00
if let Some ( input ) = app_state . config . get_input_by_name ( input_name . as_str ()) {
2025-02-05 20:01:49 +01:00
if let Some ( info_url ) = xtream ::get_xtream_player_api_info_url ( input , cluster , pli . provider_id ) {
2025-01-31 23:08:23 +01:00
// Redirect is only possible for live streams, vod and series info needs to be modified
if user . proxy == ProxyType ::Redirect && cluster == XtreamCluster ::Live {
2025-03-11 14:56:10 +01:00
return redirect ( & info_url ). into_response ();
2025-02-05 20:01:49 +01:00
} else if let Ok ( content ) = xtream ::get_xtream_stream_info ( Arc ::clone ( & app_state . http_client ), & app_state . config , user , input , target , & pli , info_url . as_str (), cluster ). await {
2025-03-11 14:56:10 +01:00
return axum ::response ::Response ::builder ()
. status ( StatusCode ::OK )
. header ( axum ::http ::header ::CONTENT_TYPE , mime ::APPLICATION_JSON . to_string ())
. body ( axum ::body ::Body ::from ( content ))
. unwrap ()
. into_response ()
2025-01-31 23:08:23 +01:00
}
2024-04-01 11:59:54 +02:00
}
2023-12-04 16:19:24 +01:00
}
}
2025-01-31 23:08:23 +01:00
return match cluster {
XtreamCluster ::Video => {
let content = create_vod_info_from_item ( user , & pli , virtual_record . last_updated );
2025-03-11 14:56:10 +01:00
axum ::response ::Response ::builder ()
. status ( StatusCode ::OK )
. header ( axum ::http ::header ::CONTENT_TYPE , mime ::APPLICATION_JSON . to_string ())
. body ( axum ::body ::Body ::from ( content ))
. unwrap ()
. into_response ()
2025-01-31 23:08:23 +01:00
}
2025-03-11 14:56:10 +01:00
XtreamCluster ::Live | XtreamCluster ::Series => axum ::response ::Response ::builder ()
. status ( StatusCode ::OK )
. header ( axum ::http ::header ::CONTENT_TYPE , mime ::APPLICATION_JSON . to_string ())
. body ( axum ::body ::Body ::from ( "{}" . as_bytes ()))
. unwrap ()
. into_response (),
2025-01-31 23:08:23 +01:00
};
2023-12-04 16:19:24 +01:00
}
2025-03-11 14:56:10 +01:00
axum ::response ::Response ::builder ()
. status ( StatusCode ::OK )
. header ( axum ::http ::header ::CONTENT_TYPE , mime ::APPLICATION_JSON . to_string ())
. body ( axum ::body ::Body ::from ( "{}" . as_bytes ()))
. unwrap ()
. into_response ()
2023-12-04 16:19:24 +01:00
}
2023-12-01 21:38:23 +01:00
2025-03-11 14:56:10 +01:00
async fn xtream_get_short_epg ( app_state : & AppState , user : & ProxyUserCredentials , target : & ConfigTarget , stream_id : & str , limit : & str ) -> impl axum ::response ::IntoResponse + Send {
2024-05-06 10:22:35 +02:00
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 ,
2025-03-11 14:56:10 +01:00
Err ( _ ) => return axum ::http ::StatusCode ::BAD_REQUEST . into_response ()
2024-05-06 10:22:35 +02:00
};
2024-03-18 17:30:52 +01:00
2025-02-06 10:49:34 +01:00
if let Ok (( pli , _ )) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , None ) {
2025-01-31 23:08:23 +01:00
if pli . provider_id > 0 {
2025-03-11 14:56:10 +01:00
let input_name = & pli . input_name ;
2025-01-31 23:08:23 +01:00
if let Some ( input ) = app_state . config . get_input_by_name ( input_name . as_str ()) {
2025-02-05 20:01:49 +01:00
if let Some ( action_url ) = xtream ::get_xtream_player_api_action_url ( input , ACTION_GET_SHORT_EPG ) {
2025-01-31 23:08:23 +01:00
let mut info_url = format! ( " {action_url} & {TAG_STREAM_ID} = {} " , pli . provider_id );
if ! ( limit . is_empty () || limit . eq ( "0" )) {
info_url = format! ( " {info_url} &limit= {limit} " );
2024-05-06 10:22:35 +02:00
}
2025-01-31 23:08:23 +01:00
if user . proxy == ProxyType ::Redirect {
2025-03-11 14:56:10 +01:00
return redirect ( & info_url ). into_response ();
2025-01-31 23:08:23 +01:00
}
2025-02-05 20:01:49 +01:00
return match request ::download_text_content ( Arc ::clone ( & app_state . http_client ), input , info_url . as_str (), None ). await {
2025-03-11 14:56:10 +01:00
Ok ( content ) => ( axum ::http ::StatusCode ::OK , axum ::Json ( content )). into_response (),
2025-01-31 23:08:23 +01:00
Err ( err ) => {
error! ( "Failed to download epg {}" , sanitize_sensitive_info ( err . to_string (). as_str ()));
2025-03-11 14:56:10 +01:00
get_empty_epg_response (). into_response ()
2025-01-31 23:08:23 +01: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}" );
2025-03-11 14:56:10 +01:00
get_empty_epg_response (). into_response ()
2023-12-01 21:38:23 +01:00
}
2025-03-11 14:56:10 +01:00
async fn xtream_player_api_handle_content_action ( config : & Config , target_name : & str , action : & str , category_id : Option < u32 > , user : & ProxyUserCredentials ) -> Option < impl IntoResponse > {
2024-05-06 10:22:35 +02:00
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 ),
2025-01-17 18:32:17 +01:00
_ => Err ( str_to_io_error ( "" ))
2024-05-06 10:22:35 +02:00
} {
if let Some ( file_path ) = path {
2025-02-22 17:11:16 +01:00
// load user bouquet
let filter = match action {
2025-03-08 06:33:38 +01:00
ACTION_GET_LIVE_CATEGORIES => user_repository ::user_get_bouquet_filter ( config , & user . username , category_id , TargetType ::Xtream , XtreamCluster ::Live ). await ,
ACTION_GET_VOD_CATEGORIES => user_repository ::user_get_bouquet_filter ( config , & user . username , category_id , TargetType ::Xtream , XtreamCluster ::Video ). await ,
ACTION_GET_SERIES_CATEGORIES => user_repository ::user_get_bouquet_filter ( config , & user . username , category_id , TargetType ::Xtream , XtreamCluster ::Series ). await ,
2025-02-22 17:11:16 +01:00
_ => None
};
if let Some ( flt ) = filter {
2025-03-11 14:56:10 +01:00
return Some ( serve_query ( & file_path , & HashMap ::from ([( TAG_CATEGORY_ID , flt )])). into_response ());
2024-05-06 10:22:35 +02:00
}
2025-03-11 14:56:10 +01:00
return Some ( serve_file ( & file_path , mime ::APPLICATION_JSON ). await . into_response ());
2024-05-06 10:22:35 +02:00
} else if let Some ( payload ) = content {
2025-03-11 14:56:10 +01:00
return Some ( axum ::response ::Response ::builder ()
. status ( axum ::http ::StatusCode ::OK )
. body ( payload ). unwrap (). into_response ());
2024-05-06 10:22:35 +02:00
}
2025-03-11 14:56:10 +01:00
return Some ( api_utils ::empty_json_list_response (). into_response ());
2024-05-06 10:22:35 +02:00
}
None
}
2025-03-11 14:56:10 +01:00
async fn xtream_get_catchup_response ( app_state : & AppState , target : & ConfigTarget , stream_id : & str , start : & str , end : & str ) -> impl axum ::response ::IntoResponse + Send {
2024-10-29 17:43:04 +01:00
let virtual_id : u32 = try_result_bad_request! ( FromStr ::from_str ( stream_id ));
2025-02-06 10:49: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 )));
2025-01-27 15:58:57 +01:00
let input = try_option_bad_request! ( app_state . config . get_input_by_name ( pli . input_name . as_str ()));
2025-02-05 20:01:49 +01:00
let info_url = try_option_bad_request! ( xtream ::get_xtream_player_api_action_url ( input , ACTION_GET_CATCHUP_TABLE ). map ( | action_url | format! ( " {action_url} & {TAG_STREAM_ID} = {} &start= {start} &end= {end} " , pli . provider_id )));
let content = try_result_bad_request! ( xtream ::get_xtream_stream_info_content ( Arc ::clone ( & app_state . http_client ), 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 ()));
2025-03-11 14:56:10 +01:00
let ( mut target_id_mapping , file_lock ) = get_target_id_mapping ( & app_state . config , & target_path ). await ;
2024-10-29 17:43:04 +01:00
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 ()) {
2025-02-04 20:05:33 +01:00
let uuid = generate_playlist_uuid ( & hex_encode ( & pli . get_uuid ()), & catchup_provider_id . to_string (), pli . item_type , & pli . url );
2025-03-11 14:56:10 +01:00
let virtual_id = target_id_mapping . get_and_update_virtual_id ( & 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
}
}
2025-02-12 13:14:45 +01:00
if let Err ( err ) = target_id_mapping . persist () {
error! ( "Failed to write catchup id mapping {err}" );
2025-03-11 14:56:10 +01:00
return axum ::http ::StatusCode ::BAD_REQUEST . into_response ();
2025-02-12 13:14:45 +01:00
}
drop ( file_lock );
2025-03-11 14:56:10 +01:00
serde_json ::to_string ( & doc )
. map_or_else (
| _ | axum ::http ::StatusCode ::BAD_REQUEST . into_response (),
| result | axum ::response ::Response ::builder ()
. status ( axum ::http ::StatusCode ::OK )
. header ( axum ::http ::header ::CONTENT_TYPE , mime ::APPLICATION_JSON . to_string ())
. body ( result ). unwrap (). into_response ())
2024-05-23 20:43:36 +02:00
}
2025-01-31 23:08:23 +01:00
macro_rules ! skip_json_response_if_flag_set {
2024-12-09 09:54:01 +01:00
( $flag :expr , $stmt :expr ) => {
if $flag {
2025-03-11 14:56:10 +01:00
return api_utils ::empty_json_list_response (). into_response ();
2024-12-09 09:54:01 +01:00
}
2025-03-11 14:56:10 +01:00
return $stmt . into_response ();
2024-12-09 09:54:01 +01:00
};
}
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
api_req : UserApiRequest ,
2025-03-11 14:56:10 +01:00
app_state : & Arc < AppState > ,
) -> impl axum ::response ::IntoResponse + Send {
2025-02-05 20:01:49 +01:00
let user_target = get_user_target ( & api_req , app_state ). await ;
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 ) {
2025-03-11 14:56:10 +01:00
return axum ::response ::Json ( get_user_info ( & user , app_state ). await ). into_response ();
2024-11-02 08:53:20 +01:00
}
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 () {
2025-03-11 14:56:10 +01:00
return axum ::response ::Json ( get_user_info ( & user , app_state ). await ). into_response ();
2024-11-02 08:53:20 +01:00
}
2025-03-16 17:08:00 +01:00
if user . permission_denied ( app_state ) {
2025-03-11 14:56:10 +01:00
return axum ::http ::StatusCode ::FORBIDDEN . into_response ();
2025-02-06 20:31:51 +01:00
}
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 => {
2025-01-31 23:08:23 +01:00
skip_json_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 => {
2025-01-31 23:08:23 +01:00
skip_json_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 (),
2025-03-11 14:56:10 +01:00
). await . into_response ();
2024-11-02 08:53:20 +01:00
}
ACTION_GET_CATCHUP_TABLE => {
2025-01-31 23:08:23 +01:00
skip_json_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
2025-03-08 12:27:06 +01:00
let category_id = api_req . category_id . trim (). parse ::< u32 > (). ok ();
2024-11-02 08:53:20 +01:00
// Handle general content actions
if let Some ( response ) = xtream_player_api_handle_content_action (
2025-03-11 14:56:10 +01:00
& app_state . config , & target . name , action , category_id , & user ,
2024-11-02 08:53:20 +01:00
). await {
2025-03-11 14:56:10 +01:00
return response . into_response ();
2024-11-02 08:53:20 +01:00
}
let result = match action {
2024-12-09 09:54:01 +01:00
ACTION_GET_LIVE_STREAMS =>
2025-02-22 17:11:16 +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-02-22 17:11:16 +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-02-22 17:11:16 +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 );
2025-03-11 14:56:10 +01:00
axum ::response ::Response ::builder ()
. status ( axum ::http ::StatusCode ::OK )
. header ( axum ::http ::header ::CONTENT_TYPE , mime ::APPLICATION_JSON . to_string ())
. body ( axum ::body ::Body ::from_stream ( content_stream )). unwrap (). into_response ()
2024-12-09 09:54:01 +01:00
}
Err ( err ) => {
error! ( "Failed response for xtream target: {} action: {} error: {}" , & target . name , action , err );
2025-01-31 23:08:23 +01:00
// Some players fail on NoContent, so we return an empty array
2025-03-11 14:56:10 +01:00
api_utils ::empty_json_list_response (). into_response ()
2024-12-09 09:54:01 +01:00
}
}
2024-11-02 08:53:20 +01:00
}
2024-12-09 09:54:01 +01:00
None => {
2025-01-31 23:08:23 +01:00
// Some players fail on NoContent, so we return an empty array
2025-03-11 14:56:10 +01:00
api_utils ::empty_json_list_response (). into_response ()
2023-10-09 00:17:54 +02:00
}
}
2024-10-30 11:14:55 +01:00
} else {
2025-01-30 17:23:41 +01:00
match ( user_target . is_none (), api_req . action . is_empty ()) {
( true , _ ) => debug! ( "Cant find user!" ),
( _ , true ) => debug! ( "Parameter action is empty!" ),
_ => debug! ( "Bad request!" ),
2024-11-21 18:11:25 +01:00
}
2025-03-11 14:56:10 +01:00
axum ::http ::StatusCode ::BAD_REQUEST . into_response ()
2023-10-09 00:17:54 +02:00
}
2023-10-20 12:18:44 +02:00
}
2023-11-02 20:03:22 +01:00
2025-03-06 20:15:08 +01:00
fn xtream_create_content_stream ( xtream_iter : impl Iterator < Item = ( String , bool ) > ) -> impl Stream < Item = Result < Bytes , String >> {
2024-12-09 09:54:01 +01:00
stream ::once ( async { Ok ::< Bytes , String > ( Bytes ::from ( "[" )) }). chain (
2025-03-06 20:15:08 +01:00
stream ::iter ( xtream_iter . map ( move | ( line , has_next ) | {
Ok ::< Bytes , String > ( Bytes ::from ( if has_next {
format! ( " {line} ," )
2024-12-09 16:43:19 +01:00
} else {
2025-03-06 20:15:08 +01:00
line . to_string ()
}))
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
}
2025-03-11 14:56:10 +01:00
async fn xtream_player_api_get (
axum ::extract ::State ( app_state ) : axum ::extract ::State < Arc < AppState >> ,
axum ::extract ::Query ( api_req ) : axum ::extract ::Query < UserApiRequest > ,
) -> impl axum ::response ::IntoResponse + Send {
xtream_player_api ( api_req , & app_state ). await
}
2025-03-16 17:08:00 +01:00
2025-03-11 14:56:10 +01:00
async fn xtream_player_api_post (
axum ::extract ::State ( app_state ) : axum ::extract ::State < Arc < AppState >> ,
axum ::extract ::Form ( api_req ) : axum ::extract ::Form < UserApiRequest > ,
) -> impl axum ::response ::IntoResponse + Send {
xtream_player_api ( api_req , & app_state ). await
}
2023-12-02 20:47:26 +01:00
2024-12-09 16:43:19 +01:00
macro_rules ! register_xtream_api {
2025-03-11 14:56:10 +01:00
( $router :expr , [ $($path :expr ), * ]) => {{
$router
2024-12-09 16:43:19 +01:00
$(
2025-03-11 14:56:10 +01:00
. route ( $path , axum ::routing ::get ( xtream_player_api_get ))
. route ( $path , axum ::routing ::post ( xtream_player_api_post ))
// $router.service(web::resource($path).route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post)))
2024-12-09 16:43:19 +01:00
) *
}};
}
macro_rules ! register_xtream_api_stream {
2025-03-11 14:56:10 +01:00
( $router :expr , [ $( ( $path :expr , $fn_name :ident )), * ]) => {{
$router
2024-12-09 16:43:19 +01:00
$(
2025-03-11 14:56:10 +01:00
. route ( format! ( " {} / {{ username }} / {{ password }} / {{ stream_id }} " , $path ). as_str (), axum ::routing ::get ( $fn_name ))
// $cfg.service(web::resource(format!("{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to($fn_name)));
2024-12-09 16:43:19 +01:00
) *
}};
}
2025-01-03 16:49:24 +01:00
macro_rules ! register_xtream_api_resource {
2025-03-11 14:56:10 +01:00
( $router :expr , [ $( ( $path :expr , $fn_name :ident )), * ]) => {{
$router
2025-01-03 16:49:24 +01:00
$(
2025-03-11 14:56:10 +01:00
. route ( format! ( "/resource/ {} / {{ username }} / {{ password }} / {{ stream_id }} / {{ resource }} " , $path ). as_str (), axum ::routing ::get ( $fn_name ))
// $cfg.service(web::resource(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path)).route(web::get().to($fn_name)));
2025-01-03 16:49:24 +01:00
) *
}};
}
2024-12-09 16:43:19 +01:00
macro_rules ! register_xtream_api_timeshift {
2025-03-11 14:56:10 +01:00
( $router :expr , [ $($path :expr ), * ]) => {{
$router
2024-12-09 16:43:19 +01:00
$(
2025-03-11 14:56:10 +01:00
. route ( $path , axum ::routing ::get ( xtream_player_api_timeshift_stream ))
. route ( $path , axum ::routing ::post ( xtream_player_api_timeshift_stream ))
//$cfg.service(web::resource($path).route(web::get().to(xtream_player_api_timeshift_stream)).route(web::post().to(xtream_player_api_timeshift_stream)));
2024-12-09 16:43:19 +01:00
) *
}};
}
2025-03-11 14:56:10 +01:00
pub fn xtream_api_register () -> axum ::Router < Arc < AppState >> {
let router = axum ::Router ::new ();
let mut router = register_xtream_api! ( router , [ "/player_api.php" , "/panel_api.php" , "/xtream" ]);
router = register_xtream_api_stream! ( router , [
2024-12-09 16:43:19 +01:00
( "" , xtream_player_api_live_stream_alt ),
( "/live" , xtream_player_api_live_stream ),
( "/movie" , xtream_player_api_movie_stream ),
( "/series" , xtream_player_api_series_stream )]);
2025-03-11 14:56:10 +01:00
router = register_xtream_api_timeshift! ( router , [
2024-12-09 16:43:19 +01:00
"/timeshift/{username}/{password}/{duration}/{start}/{stream_id}" ,
2024-12-29 11:36:44 +01:00
"/timeshift.php" ,
"/streaming/timeshift.php" ]);
2025-03-11 14:56:10 +01:00
register_xtream_api_resource! ( router , [
2025-01-03 16:49:24 +01:00
( "live" , xtream_player_api_live_resource ),
( "movie" , xtream_player_api_movie_resource ),
2025-03-11 14:56:10 +01:00
( "series" , xtream_player_api_series_resource )])
2023-11-02 20:03:22 +01:00
}