2025-06-03 18:25:15 +02:00
use crate ::api ::api_utils ::{ force_provider_stream_response , get_user_target , get_user_target_by_credentials , is_seek_request , redirect , redirect_response , resource_response , separate_number_and_remainder , stream_response , try_option_bad_request , try_result_bad_request , RedirectParams };
2025-02-05 20:01:49 +01:00
use crate ::api ::endpoints ::hls_api ::handle_hls_stream_request ;
2025-06-03 18:25:15 +02:00
use crate ::api ::endpoints ::xtream_api ::{ ApiStreamContext , ApiStreamRequest };
2024-12-05 19:54:34 +01:00
use crate ::api ::model ::app_state ::AppState ;
use crate ::api ::model ::request ::UserApiRequest ;
2025-06-03 18:25:15 +02:00
use crate ::api ::model ::streams ::provider_stream ::{ create_custom_video_stream_response , CustomVideoStreamType };
2025-06-19 17:20:31 +02:00
use shared ::model ::{ FieldGetAccessor , PlaylistEntry , PlaylistItemType , TargetType , UserConnectionPermission , XtreamCluster };
2025-01-31 23:08:23 +01:00
use crate ::repository ::m3u_repository ::{ m3u_get_item_for_stream_id , m3u_load_rewrite_playlist };
2025-06-03 18:25:15 +02:00
use crate ::repository ::storage_const ;
2025-05-04 10:59:08 +02:00
use crate ::utils ::request ::{ extract_extension_from_url , sanitize_sensitive_info };
2025-06-19 13:22:43 +02:00
use shared ::utils ::HLS_EXT ;
2025-06-03 18:25:15 +02:00
use axum ::http ::{ HeaderMap , StatusCode };
2025-03-11 14:56:10 +01:00
use axum ::response ::IntoResponse ;
use bytes ::Bytes ;
use futures ::stream ;
use log ::{ debug , error };
use std ::sync ::Arc ;
2025-06-05 17:51:03 +02:00
use crate ::auth ::Fingerprint ;
2023-11-02 20:03:22 +01:00
2024-12-05 19:54:34 +01:00
async fn m3u_api (
2024-12-03 14:54:42 +01:00
api_req : & UserApiRequest ,
app_state : & AppState ,
2025-03-11 14:56:10 +01:00
) -> impl axum ::response ::IntoResponse + Send {
2025-05-14 09:44:56 +02:00
match get_user_target ( api_req , app_state ) {
2024-04-01 16:49:12 +02:00
Some (( user , target )) => {
2024-12-05 19:54:34 +01:00
match m3u_load_rewrite_playlist ( & app_state . config , target , & user ). await {
2024-11-01 11:14:04 +01:00
Ok ( m3u_iter ) => {
// Convert the iterator into a stream of `Bytes`
2025-03-06 20:15:08 +01:00
let content_stream = stream ::iter ( m3u_iter . map ( | line | Ok ::< Bytes , String > ( Bytes ::from ([ line . to_string (). as_bytes (), b " \n " ]. concat ()))));
2025-03-11 14:56:10 +01:00
let mut builder = axum ::response ::Response ::builder ()
. status ( axum ::http ::StatusCode ::OK )
. header ( axum ::http ::header ::CONTENT_TYPE , mime ::TEXT_PLAIN_UTF_8 . to_string ());
2025-02-04 10:25:40 +01:00
if api_req . content_type == "m3u_plus" {
2025-03-11 14:56:10 +01:00
builder = builder . header ( "Content-Disposition" , "attachment; filename= \" playlist.m3u \" " );
2025-02-04 10:25:40 +01:00
}
2025-03-11 14:56:10 +01:00
builder . body ( axum ::body ::Body ::from_stream ( content_stream )). unwrap (). into_response ()
2024-11-01 11:14:04 +01:00
}
Err ( err ) => {
2025-01-23 11:50:27 +01:00
error! ( "{}" , sanitize_sensitive_info ( err . to_string (). as_str ()));
2025-03-11 14:56:10 +01:00
axum ::http ::StatusCode ::NO_CONTENT . into_response ()
2024-11-01 11:14:04 +01:00
}
2023-10-20 12:18:44 +02:00
}
}
2025-03-11 14:56:10 +01:00
None => axum ::http ::StatusCode ::BAD_REQUEST . into_response (),
2023-10-20 12:18:44 +02:00
}
2023-11-02 20:03:22 +01:00
}
2025-03-11 14:56:10 +01:00
async fn m3u_api_get ( axum ::extract ::Query ( api_req ) : axum ::extract ::Query < UserApiRequest > ,
axum ::extract ::State ( app_state ) : axum ::extract ::State < Arc < AppState >> ,
) -> impl axum ::response ::IntoResponse + Send {
m3u_api ( & api_req , & app_state ). await
2024-11-22 09:58:28 +01:00
}
2025-03-11 14:56:10 +01:00
2024-11-22 09:58:28 +01:00
async fn m3u_api_post (
2025-03-11 14:56:10 +01:00
axum ::extract ::State ( app_state ) : axum ::extract ::State < Arc < AppState >> ,
axum ::extract ::Form ( api_req ) : axum ::extract ::Form < UserApiRequest > ,
) -> impl axum ::response ::IntoResponse + Send {
m3u_api ( & api_req , & app_state ). await . into_response ()
2024-11-22 09:58:28 +01:00
}
2024-04-01 18:02:02 +02:00
async fn m3u_api_stream (
2025-06-05 17:51:03 +02:00
fingerprint : & str ,
2025-06-03 18:25:15 +02:00
req_headers : & HeaderMap ,
app_state : & Arc < AppState > ,
2025-06-05 17:51:03 +02:00
api_req : & UserApiRequest ,
2025-06-03 18:25:15 +02:00
stream_req : ApiStreamRequest < '_ > ,
// _addr: &std::net::SocketAddr,
2025-03-11 14:56:10 +01:00
) -> impl axum ::response ::IntoResponse + Send {
2025-06-03 18:25:15 +02: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 ));
if user . permission_denied ( app_state ) {
2025-05-26 13:57:29 +02:00
return create_custom_video_stream_response ( & app_state . config , CustomVideoStreamType ::UserAccountExpired ). into_response ();
2025-02-06 20:31:51 +01:00
}
2024-12-09 16:43:19 +01:00
2025-04-24 18:12:29 +02:00
let target_name = & target . name ;
2024-12-09 16:43:19 +01:00
if ! target . has_output ( & TargetType ::M3u ) {
2025-04-30 09:55:08 +02:00
debug! ( "Target has no m3u playlist {target_name}" );
2025-04-24 18:12:29 +02:00
return StatusCode ::BAD_REQUEST . into_response ();
2024-12-09 16:43:19 +01:00
}
2025-06-03 18:25:15 +02:00
let ( action_stream_id , stream_ext ) = separate_number_and_remainder ( stream_req . stream_id );
2025-04-24 18:12:29 +02:00
let virtual_id : u32 = try_result_bad_request! ( action_stream_id . trim (). parse ());
let pli = try_result_bad_request! ( m3u_get_item_for_stream_id ( virtual_id , & app_state . config , target ). await , true , format! ( "Failed to read m3u item for stream id {} " , virtual_id ));
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} , stream_id {virtual_id} " ));
let cluster = XtreamCluster ::try_from ( pli . item_type ). unwrap_or ( XtreamCluster ::Live );
2025-04-22 17:08:31 +02:00
2025-06-05 17:51:03 +02:00
let session_key = format! ( " {fingerprint}{virtual_id} " );
let user_session = app_state . active_users . get_user_session ( & user . username , & session_key ). await ;
2025-04-30 09:55:08 +02:00
2025-06-04 21:40:14 +02:00
let session_url = if let Some ( session ) = & user_session {
2025-04-30 09:55:08 +02:00
if session . permission == UserConnectionPermission ::Exhausted {
2025-05-26 13:57:29 +02:00
return create_custom_video_stream_response ( & app_state . config , CustomVideoStreamType ::UserConnectionsExhausted ). into_response ();
2025-04-30 09:55:08 +02:00
}
2025-04-22 17:08:31 +02:00
2025-04-30 09:55:08 +02:00
if app_state . active_provider . is_over_limit ( & session . provider ). await {
2025-05-26 13:57:29 +02:00
return create_custom_video_stream_response ( & app_state . config , CustomVideoStreamType ::ProviderConnectionsExhausted ). into_response ();
2025-04-30 09:55:08 +02:00
}
2025-06-05 08:16:52 +02:00
if session . virtual_id == virtual_id && is_seek_request ( cluster , req_headers ). await {
2025-04-30 09:55:08 +02:00
// partial request means we are in reverse proxy mode, seek happened
2025-06-05 08:16:52 +02:00
return force_provider_stream_response ( app_state , session , pli . item_type , req_headers , input , & user ). await . into_response ();
2025-04-30 09:55:08 +02:00
}
2025-06-04 21:40:14 +02:00
session . stream_url . as_str ()
} else {
pli . url . as_str ()
};
2025-04-30 09:55:08 +02:00
2025-06-05 08:16:52 +02:00
let connection_permission = user . connection_permission ( app_state ). await ;
2025-04-22 17:08:31 +02:00
if connection_permission == UserConnectionPermission ::Exhausted {
2025-05-26 13:57:29 +02:00
return create_custom_video_stream_response ( & app_state . config , CustomVideoStreamType ::UserConnectionsExhausted ). into_response ();
2025-04-22 17:08:31 +02:00
}
2025-06-03 18:25:15 +02:00
let context = ApiStreamContext ::try_from ( cluster ). unwrap_or ( ApiStreamContext ::Live );
2025-04-19 13:50:41 +02:00
let redirect_params = RedirectParams {
2025-04-24 18:12:29 +02:00
item : & pli ,
provider_id : pli . get_provider_id (),
2025-04-19 13:50:41 +02:00
cluster ,
2025-05-27 12:42:11 +02:00
target_type : TargetType ::M3u ,
2025-04-19 13:50:41 +02:00
target ,
input ,
user : & user ,
stream_ext : stream_ext . as_deref (),
req_context : context ,
2025-06-03 18:25:15 +02:00
action_path : "" , // TODO is there timeshoft or something like that ?
2025-04-19 13:50:41 +02:00
};
2025-01-31 23:08:23 +01:00
2025-06-05 08:16:52 +02:00
if let Some ( response ) = redirect_response ( app_state , & redirect_params ). await {
2025-04-19 13:50:41 +02:00
return response . into_response ();
2025-01-31 23:08:23 +01:00
}
2025-03-19 09:54:59 +01:00
2025-04-24 18:12:29 +02:00
let extension = stream_ext . unwrap_or_else (
|| extract_extension_from_url ( & pli . url ). map_or_else ( String ::new , std ::string ::ToString ::to_string ));
let is_hls_request = pli . item_type == PlaylistItemType ::LiveHls || pli . item_type == PlaylistItemType ::LiveDash || extension == HLS_EXT ;
2025-01-31 23:08:23 +01:00
// Reverse proxy mode
if is_hls_request {
2025-06-05 17:51:03 +02:00
return handle_hls_stream_request ( fingerprint , app_state , & user , user_session . as_ref (), & pli . url , pli . virtual_id , input , connection_permission ). await . into_response ();
2024-04-01 18:02:02 +02:00
}
2024-12-09 16:43:19 +01:00
2025-06-09 13:35:44 +02:00
stream_response ( app_state , & session_key , pli . virtual_id , pli . item_type , session_url , req_headers , input , target , & user , connection_permission ). await . into_response ()
2024-12-09 16:43:19 +01:00
}
2025-01-03 13:08:46 +01:00
async fn m3u_api_resource (
2025-03-11 14:56:10 +01:00
req_headers : axum ::http ::HeaderMap ,
axum ::extract ::Query ( api_req ) : axum ::extract ::Query < UserApiRequest > ,
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 >> ,
) -> impl axum ::response ::IntoResponse + Send {
let Ok ( m3u_stream_id ) = stream_id . parse ::< u32 > () else { return axum ::http ::StatusCode ::BAD_REQUEST . into_response () };
2025-05-14 09:44:56 +02:00
let Some (( user , target )) = get_user_target_by_credentials ( & username , & password , & api_req , & app_state )
2025-04-30 09:55:08 +02:00
else { return StatusCode ::BAD_REQUEST . into_response () };
2025-03-16 17:08:00 +01:00
if user . permission_denied ( & app_state ) {
2025-04-30 09:55:08 +02:00
return StatusCode ::FORBIDDEN . into_response ();
2025-02-06 20:31:51 +01:00
}
2025-01-03 13:08:46 +01:00
2025-04-30 09:55:08 +02:00
let target_name = & target . name ;
2025-01-03 13:08:46 +01:00
if ! target . has_output ( & TargetType ::M3u ) {
2025-04-30 09:55:08 +02:00
debug! ( "Target has no m3u playlist {target_name}" );
return StatusCode ::BAD_REQUEST . into_response ();
2025-01-03 13:08:46 +01:00
}
2025-01-31 23:08:23 +01:00
let m3u_item = match m3u_get_item_for_stream_id ( m3u_stream_id , & app_state . config , target ). await {
2025-01-03 13:08:46 +01:00
Ok ( item ) => item ,
Err ( err ) => {
2025-01-23 11:50:27 +01:00
error! ( "Failed to get m3u url: {}" , sanitize_sensitive_info ( err . to_string (). as_str ()));
2025-03-11 14:56:10 +01:00
return axum ::http ::StatusCode ::BAD_REQUEST . into_response ();
2025-01-03 13:08:46 +01:00
}
};
let stream_url = m3u_item . get_field ( resource . as_str ());
match stream_url {
2025-03-11 14:56:10 +01:00
None => axum ::http ::StatusCode ::NOT_FOUND . into_response (),
2025-01-03 13:08:46 +01:00
Some ( url ) => {
2025-06-03 18:25:15 +02:00
if user . proxy . is_redirect ( m3u_item . item_type ) || target . is_force_redirect ( m3u_item . item_type ) {
2025-01-23 11:50:27 +01:00
debug! ( "Redirecting stream request to {}" , sanitize_sensitive_info ( & url ));
2025-05-23 00:27:35 +02:00
redirect ( & url ). into_response ()
2025-01-03 13:08:46 +01:00
} else {
2025-05-23 00:27:35 +02:00
resource_response ( & app_state , & url , & req_headers , None ). await . into_response ()
2025-01-03 13:08:46 +01:00
}
}
}
}
2025-06-03 18:25:15 +02:00
macro_rules ! create_m3u_api_stream {
( $fn_name :ident , $context :expr ) => {
async fn $fn_name (
2025-06-05 17:51:03 +02:00
Fingerprint ( fingerprint ) : Fingerprint ,
2025-06-03 18:25:15 +02:00
req_headers : axum ::http ::HeaderMap ,
axum ::extract ::Query ( api_req ) : axum ::extract ::Query < UserApiRequest > ,
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::ConnectInfo(addr): axum::extract::ConnectInfo<std::net::SocketAddr>,
) -> impl IntoResponse + Send {
2025-06-05 17:51:03 +02:00
m3u_api_stream (
& fingerprint ,
& req_headers ,
2025-06-03 18:25:15 +02:00
& app_state ,
& api_req ,
ApiStreamRequest ::from ( $context , & username , & password , & stream_id , "" ),
). await . into_response ()
}
}
}
create_m3u_api_stream! ( m3u_api_live_stream_alt , ApiStreamContext ::LiveAlt );
create_m3u_api_stream! ( m3u_api_live_stream , ApiStreamContext ::Live );
create_m3u_api_stream! ( m3u_api_series_stream , ApiStreamContext ::Series );
create_m3u_api_stream! ( m3u_api_movie_stream , ApiStreamContext ::Movie );
macro_rules ! register_m3u_api_stream {
( $router :expr , [ $( ( $path :expr , $fn_name :ident )), * ]) => {{
$router
$(
. route ( & format! ( "/ {} / {{ username }} / {{ password }} / {{ stream_id }} " , $path ), axum ::routing ::get ( $fn_name ))
2025-03-11 14:56:10 +01:00
// $cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to(m3u_api_stream)));
2024-12-09 16:43:19 +01:00
) *
}};
}
macro_rules ! register_m3u_api_routes {
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 ( & format! ( "/ {} " , $path ), axum ::routing ::get ( m3u_api_get ))
. route ( & format! ( "/ {} " , $path ), axum ::routing ::post ( m3u_api_post ))
// $cfg.service(web::resource(format!("/{}", $path)).route(web::get().to(m3u_api_get)).route(web::post().to(m3u_api_post)));
2024-12-09 16:43:19 +01:00
) *
}};
2024-04-01 16:49:12 +02:00
}
2025-03-11 14:56:10 +01:00
pub fn m3u_api_register () -> axum ::Router < Arc < AppState >> {
let mut router = axum ::Router ::new ();
router = register_m3u_api_routes! ( router , [ "get.php" , "apiget" , "m3u" ]);
2025-06-03 18:25:15 +02:00
router = register_m3u_api_stream! ( router , [
( storage_const ::M3U_STREAM_PATH , m3u_api_live_stream_alt ),
( format! ( " {} /live" , storage_const ::M3U_STREAM_PATH ), m3u_api_live_stream ),
( format! ( " {} /movie" , storage_const ::M3U_STREAM_PATH ), m3u_api_movie_stream ),
( format! ( " {} /series" , storage_const ::M3U_STREAM_PATH ), m3u_api_series_stream )]);
router
. route ( & format! ( "/ {} / {{ username }} / {{ password }} / {{ stream_id }} / {{ resource }} " , storage_const ::M3U_RESOURCE_PATH ), axum ::routing ::get ( m3u_api_resource ))
2023-10-20 12:18:44 +02:00
}