2023-10-09 00:17:54 +02:00
// https://github.com/tellytv/go.xtream-codes/blob/master/structs.go
2025-01-31 23:08:23 +01:00
use crate ::{ trace_if_enabled , try_option_bad_request , try_result_bad_request , Arc };
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-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-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 };
use crate ::api ::hls_api ::handle_hls_stream_request ;
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-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-01-31 23:08:23 +01:00
use crate ::repository ::playlist_repository ::HLS_EXT ;
2025-01-29 17:33:06 +01:00
use crate ::repository ::storage ::{ get_target_storage_path , hex_encode };
2024-10-29 17:43:04 +01:00
use crate ::repository ::target_id_mapping ::TargetIdMapping ;
2025-01-29 17:33:06 +01:00
use crate ::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-01-31 23:08:23 +01:00
use crate ::utils ::hash_utils ::{ generate_playlist_uuid };
use crate ::utils ::json_utils ::{ get_u32_from_serde_value };
use crate ::utils ::request_utils ::{ extract_extension_from_url , replace_extension , sanitize_sensitive_info };
use crate ::utils ::xtream_utils ::{ 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 };
2025-01-28 13:04:58 +01:00
use crate ::utils ::{ json_utils , request_utils , xtream_utils };
2025-01-05 18:22:23 +01:00
use crate ::{ debug_if_enabled , info_err };
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-01-30 12:30:04 +01:00
#[derive(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-01-30 12:30:04 +01:00
#[derive(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 ,
}
}
}
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
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 > {
2024-04-01 11:59:54 +02:00
if let Some ( user_info ) = input . get_user_info () {
2025-02-03 23:54:08 +01:00
let ctx = match context {
XtreamApiStreamContext ::LiveAlt |
XtreamApiStreamContext ::Live => {
let use_prefix = input . options . as_ref (). map_or ( true , | o | o . xtream_live_stream_use_prefix );
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! ( " {} / {}{} / {} / {} " ,
& 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 );
2025-02-03 23:54:08 +01:00
2024-11-01 11:14:04 +01:00
XtreamAuthorizationResponse ::new ( & server_info , user )
2023-10-09 00:17:54 +02: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 ();
}
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-01-26 13:16:07 +01:00
let ( pli , mapping ) = 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-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
2024-12-02 17:24:06 +01:00
if pli . item_type == PlaylistItemType ::LiveHls {
2025-01-25 19:27:11 +01:00
debug_if_enabled! ( "Redirecting stream request to {}" , sanitize_sensitive_info ( & pli . url ));
return HttpResponse ::Found (). insert_header (( "Location" , pli . url . to_string ())). finish ();
2024-11-10 14:17:24 +01:00
}
2025-01-31 23:08:23 +01:00
let is_hls_request = stream_ext . as_deref () == Some ( HLS_EXT );
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 );
return HttpResponse ::Found (). insert_header (( "Location" , stream_url )). finish ();
}
2025-01-31 23:08:23 +01:00
let redirect_url = if is_hls_request { & replace_extension ( & pli . url , "m3u8" ) } else { & pli . url };
debug_if_enabled! ( "Redirecting stream request to {}" , sanitize_sensitive_info ( redirect_url ));
return HttpResponse ::Found (). insert_header (( "Location" , redirect_url . as_str ())). finish ();
}
// Reverse proxy mode
if is_hls_request {
return handle_hls_stream_request ( app_state , & user , & pli , input , TargetType ::Xtream ). await ;
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-01-07 14:34:33 +01:00
stream_response ( app_state , & stream_url , req , Some ( input ), pli . item_type , target ). await
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-01-16 17:32:17 +01:00
fn get_doc_resource_field_value ( field : & str , doc : Option <& Value > ) -> Option < Rc < String >> {
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 ) {
return Some ( Rc ::new ( url . to_string ()));
}
}
None
}
2025-01-05 18:22:23 +01:00
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 ) {
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
}
async fn xtream_get_season_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 ::Series => {
xtream_repository ::xtream_load_series_info ( config , target . name . as_str (), pli . get_virtual_id ()). await
}
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 (
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-26 13:16:07 +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 )
2025-01-16 17:32:17 +01:00
} else if resource . starts_with ( SEASON_RESOURCE_PREFIX ) {
try_result_bad_request! ( xtream_get_season_resource_url ( & app_state . config , & pli , target , resource ). await )
2025-01-05 18:22:23 +01:00
} 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 {
2025-01-31 23:08:23 +01:00
trace_if_enabled! ( "Redirecting resource request to {}" , sanitize_sensitive_info ( & url ));
2025-01-03 16:49:24 +01:00
HttpResponse ::Found (). insert_header (( "Location" , url . as_str ())). finish ()
} else {
2025-01-31 23:08:23 +01:00
trace_if_enabled! ( "Resource request to {}" , sanitize_sensitive_info ( & url ));
2025-01-03 16:49:24 +01:00
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
2025-01-31 23:08:23 +01:00
if let Ok (( pli , virtual_record )) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , Some ( cluster )). await {
if pli . provider_id > 0 {
let input_name = Rc ::clone ( & pli . input_name );
if let Some ( input ) = app_state . config . get_input_by_name ( input_name . as_str ()) {
if let Some ( info_url ) = xtream_utils ::get_xtream_player_api_info_url ( input , cluster , pli . provider_id ) {
// Redirect is only possible for live streams, vod and series info needs to be modified
if user . proxy == ProxyType ::Redirect && cluster == XtreamCluster ::Live {
return HttpResponse ::Found (). insert_header (( "Location" , info_url )). finish ();
} else if let Ok ( content ) = xtream_utils ::get_xtream_stream_info ( Arc ::clone ( & app_state . http_client ), & app_state . config , user , input , target , & pli , info_url . as_str (), cluster ). await {
return HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( content );
}
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 );
HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( content )
}
XtreamCluster ::Live | XtreamCluster ::Series => HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( "{}" ),
};
2023-12-04 16:19:24 +01:00
}
2025-01-31 23:08:23 +01:00
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
2025-01-26 13:16:07 +01:00
if let Ok (( pli , _ )) = xtream_repository ::xtream_get_item_for_stream_id ( virtual_id , & app_state . config , target , None ). await {
2025-01-31 23:08:23 +01:00
if pli . provider_id > 0 {
let input_name = Rc ::clone ( & pli . input_name );
if let Some ( input ) = app_state . config . get_input_by_name ( input_name . as_str ()) {
if let Some ( action_url ) = xtream_utils ::get_xtream_player_api_action_url ( input , ACTION_GET_SHORT_EPG ) {
let mut info_url = format! ( " {action_url} & {TAG_STREAM_ID} = {} " , pli . provider_id );
if ! ( limit . is_empty () || limit . eq ( "0" )) {
info_url = format! ( " {info_url} &limit= {limit} " );
2024-05-06 10:22:35 +02:00
}
2025-01-31 23:08:23 +01:00
if user . proxy == ProxyType ::Redirect {
return HttpResponse ::Found (). insert_header (( "Location" , info_url )). finish ();
}
return match request_utils ::download_text_content ( Arc ::clone ( & app_state . http_client ), input , info_url . as_str (), None ). await {
Ok ( content ) => HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( content ),
Err ( err ) => {
error! ( "Failed to download epg {}" , sanitize_sensitive_info ( err . to_string (). as_str ()));
HttpResponse ::NoContent (). finish ()
}
};
}
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 ),
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 {
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 ));
2025-01-26 13:16:07 +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 );
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-01-28 13:04:58 +01:00
let info_url = try_option_bad_request! ( xtream_utils ::get_xtream_player_api_action_url ( input , ACTION_GET_CATCHUP_TABLE ). map ( | action_url | format! ( " {action_url} & {TAG_STREAM_ID} = {} &start= {start} &end= {end} " , pli . provider_id )));
let content = try_result_bad_request! ( xtream_utils ::get_xtream_stream_info_content ( Arc ::clone ( & app_state . http_client ), info_url . as_str (), input ). await );
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-01-31 23:08:23 +01:00
let mut target_id_mapping = {
let _file_lock = match app_state . config . file_locks . read_lock ( & target_path ). await {
Ok ( lock ) => lock ,
Err ( err ) => {
error! ( "Could not get lock for id mapping for target {} err:{err}" , target . name );
return HttpResponse ::InternalServerError (). finish ();
}
};
TargetIdMapping ::new ( & target_path )
};
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-01-29 17:33:06 +01:00
let uuid = generate_playlist_uuid ( & hex_encode ( & pli . get_uuid ()), & catchup_provider_id . to_string (), & pli . url );
2025-01-31 23:08:23 +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-01-31 23:08:23 +01:00
{
let _file_lock = match app_state . config . file_locks . write_lock ( & target_path ). await {
Ok ( lock ) => lock ,
Err ( err ) => {
error! ( "Could not get lock for id mapping for target {} err:{err}" , target . name );
return HttpResponse ::InternalServerError (). finish ();
}
};
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
}
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-01-31 23:08:23 +01:00
return HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( "[]" );
2024-12-09 09:54:01 +01:00
}
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 => {
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 (),
2024-11-02 08:53:20 +01:00
). await ;
}
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
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 );
2025-01-31 23:08:23 +01:00
// Some players fail on NoContent, so we return an empty array
HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( "[]" )
// HttpResponse::NoContent().finish()
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
HttpResponse ::Ok (). content_type ( mime ::APPLICATION_JSON ). body ( "[]" )
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
}
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 )]);
2023-11-02 20:03:22 +01:00
}