XC Api post reuqest support (#661)

* XC Api post reuqest support
This commit is contained in:
euzu
2026-03-22 19:55:14 +01:00
committed by GitHub
parent 90193d7260
commit 65b774dee3
5 changed files with 400 additions and 50 deletions
+220 -45
View File
@@ -686,16 +686,6 @@ create_xtream_player_api_resource!(xtream_player_api_live_resource, ApiStreamCon
create_xtream_player_api_resource!(xtream_player_api_series_resource, ApiStreamContext::Series);
create_xtream_player_api_resource!(xtream_player_api_movie_resource, ApiStreamContext::Movie);
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
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
struct XtreamApiTimeShiftRequest {
username: String,
@@ -708,40 +698,51 @@ struct XtreamApiTimeShiftRequest {
async fn xtream_player_api_timeshift_stream(
fingerprint: Fingerprint,
req_headers: HeaderMap,
axum::extract::Query(mut api_req): axum::extract::Query<UserApiRequest>,
axum::extract::Query(api_query_req): axum::extract::Query<UserApiRequest>,
axum::extract::Path(timeshift_request): axum::extract::Path<XtreamApiTimeShiftRequest>,
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
axum::extract::Form(api_form_req): axum::extract::Form<UserApiRequest>,
api_form_req: Result<axum::extract::Form<UserApiRequest>, axum::extract::rejection::FormRejection>,
) -> impl IntoResponse + Send {
let username = get_non_empty(&timeshift_request.username, &api_req.username, &api_form_req.username).to_string();
let password = get_non_empty(&timeshift_request.password, &api_req.password, &api_form_req.password).to_string();
let stream_id =
get_non_empty(&timeshift_request.stream_id, &api_req.stream_id, &api_form_req.stream_id).to_string();
let duration = get_non_empty(&timeshift_request.duration, &api_req.duration, &api_form_req.duration);
let start_time = get_non_empty(&timeshift_request.start, &api_req.start, &api_form_req.start);
let form_req = api_form_req.as_ref().ok().map(|form| &form.0);
let query_req = UserApiRequest::merge_query_over_form(&api_query_req, form_req);
let path_req = UserApiRequest {
username: timeshift_request.username,
password: timeshift_request.password,
duration: timeshift_request.duration,
start: timeshift_request.start,
stream_id: timeshift_request.stream_id,
..UserApiRequest::default()
};
let api_req = UserApiRequest::merge_prefer_primary(&path_req, &query_req);
let auth_status = app_state.app_config.get_auth_error_status();
let (user, target) = try_option_forbidden!(
get_user_target_by_credentials(&username, &password, &api_form_req, &app_state),
get_user_target_by_credentials(&api_req.username, &api_req.password, &api_req, &app_state),
auth_status,
false,
format!("Could not find any user {username}")
format!("Could not find any user {}", api_req.username)
);
let epg_timeshift = parse_timeshift(user.epg_request_timeshift.as_deref());
let start = apply_timeshift(start_time, &epg_timeshift);
let action_path = if start.is_empty() { format!("{duration}/{start_time}") } else { format!("{duration}/{start}") };
api_req.username.clone_from(&username);
api_req.password.clone_from(&password);
api_req.stream_id.clone_from(&stream_id);
let start = apply_timeshift(&api_req.start, &epg_timeshift);
let action_path = if start.is_empty() {
format!("{}/{}", api_req.duration, api_req.start)
} else {
format!("{}/{}", api_req.duration, start)
};
xtream_player_api_stream(
&fingerprint,
&req_headers,
&app_state,
&api_req,
ApiStreamRequest::from(ApiStreamContext::Timeshift, &username, &password, &stream_id, &action_path),
ApiStreamRequest::from(
ApiStreamContext::Timeshift,
&api_req.username,
&api_req.password,
&api_req.stream_id,
&action_path,
),
Some((user, target)),
)
.await
@@ -753,41 +754,48 @@ async fn xtream_player_api_timeshift_query_stream(
req_headers: HeaderMap,
axum::extract::Query(api_query_req): axum::extract::Query<UserApiRequest>,
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
axum::extract::Form(api_form_req): axum::extract::Form<UserApiRequest>,
api_form_req: Result<axum::extract::Form<UserApiRequest>, axum::extract::rejection::FormRejection>,
) -> impl IntoResponse + Send {
let username = get_non_empty(&api_query_req.username, &api_form_req.username, "");
let password = get_non_empty(&api_query_req.password, &api_form_req.password, "");
let stream_id = get_non_empty(&api_query_req.stream, &api_form_req.stream, "");
let duration = get_non_empty(&api_query_req.duration, &api_form_req.duration, "");
let start_time = get_non_empty(&api_query_req.start, &api_form_req.start, "");
let form_req = api_form_req.as_ref().ok().map(|form| &form.0);
let api_req = UserApiRequest::merge_query_over_form(&api_query_req, form_req);
if username.is_empty()
|| password.is_empty()
|| stream_id.is_empty()
|| duration.is_empty()
|| start_time.is_empty()
if api_req.username.is_empty()
|| api_req.password.is_empty()
|| api_req.stream.is_empty()
|| api_req.duration.is_empty()
|| api_req.start.is_empty()
{
return axum::http::StatusCode::BAD_REQUEST.into_response();
}
let auth_status = app_state.app_config.get_auth_error_status();
let (user, target) = try_option_forbidden!(
get_user_target_by_credentials(username, password, &api_query_req, &app_state),
get_user_target_by_credentials(&api_req.username, &api_req.password, &api_req, &app_state),
auth_status,
false,
format!("Could not find any user {username}")
format!("Could not find any user {}", api_req.username)
);
let epg_timeshift = parse_timeshift(user.epg_request_timeshift.as_deref());
let start = apply_timeshift(start_time, &epg_timeshift);
let action_path = if start.is_empty() { format!("{duration}/{start_time}") } else { format!("{duration}/{start}") };
let start = apply_timeshift(&api_req.start, &epg_timeshift);
let action_path = if start.is_empty() {
format!("{}/{}", api_req.duration, api_req.start)
} else {
format!("{}/{}", api_req.duration, start)
};
xtream_player_api_stream(
&fingerprint,
&req_headers,
&app_state,
&api_query_req,
ApiStreamRequest::from(ApiStreamContext::Timeshift, username, password, stream_id, &action_path),
&api_req,
ApiStreamRequest::from(
ApiStreamContext::Timeshift,
&api_req.username,
&api_req.password,
&api_req.stream,
&action_path,
),
Some((user, target)),
)
.await
@@ -1305,8 +1313,11 @@ async fn xtream_player_api_get(
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>,
axum::extract::Query(api_query_req): axum::extract::Query<UserApiRequest>,
api_form_req: Result<axum::extract::Form<UserApiRequest>, axum::extract::rejection::FormRejection>,
) -> impl IntoResponse + Send {
let form_req = api_form_req.as_ref().ok().map(|form| &form.0);
let api_req = UserApiRequest::merge_query_over_form(&api_query_req, form_req);
xtream_player_api(api_req, &app_state).await
}
@@ -1398,3 +1409,167 @@ pub fn xtream_api_register() -> axum::Router<Arc<AppState>> {
]
)
}
#[cfg(test)]
mod tests {
use crate::api::model::UserApiRequest;
use super::XtreamApiTimeShiftRequest;
#[test]
fn post_query_only_request_prefers_query_when_form_is_missing() {
let api_query_req = UserApiRequest {
username: String::from("query-user"),
password: String::from("query-pass"),
action: String::from("get_live_streams"),
..UserApiRequest::default()
};
let api_req = UserApiRequest::merge_query_over_form(&api_query_req, None);
assert_eq!(api_req.username, "query-user");
assert_eq!(api_req.password, "query-pass");
assert_eq!(api_req.action, "get_live_streams");
}
#[test]
fn post_request_prefers_query_over_form() {
let api_query_req = UserApiRequest {
username: String::from("query-user"),
action: String::from("query-action"),
..UserApiRequest::default()
};
let form_req = UserApiRequest {
username: String::from("form-user"),
action: String::from("form-action"),
..UserApiRequest::default()
};
let api_req = UserApiRequest::merge_query_over_form(&api_query_req, Some(&form_req));
assert_eq!(api_req.username, "query-user");
assert_eq!(api_req.action, "query-action");
}
#[test]
fn timeshift_query_request_prefers_query_when_form_is_missing() {
let api_query_req = UserApiRequest {
username: String::from("query-user"),
password: String::from("query-pass"),
stream: String::from("42"),
duration: String::from("60"),
start: String::from("2024-01-01:00-00"),
..UserApiRequest::default()
};
let api_req = UserApiRequest::merge_query_over_form(&api_query_req, None);
assert_eq!(api_req.username, "query-user");
assert_eq!(api_req.password, "query-pass");
assert_eq!(api_req.stream, "42");
assert_eq!(api_req.duration, "60");
assert_eq!(api_req.start, "2024-01-01:00-00");
}
#[test]
fn timeshift_query_request_prefers_query_over_form() {
let api_query_req = UserApiRequest {
username: String::from("query-user"),
stream: String::from("42"),
duration: String::from("60"),
start: String::from("2024-01-01:00-00"),
..UserApiRequest::default()
};
let form_req = UserApiRequest {
username: String::from("form-user"),
stream: String::from("99"),
duration: String::from("10"),
start: String::from("form-start"),
..UserApiRequest::default()
};
let api_req = UserApiRequest::merge_query_over_form(&api_query_req, Some(&form_req));
assert_eq!(api_req.username, "query-user");
assert_eq!(api_req.stream, "42");
assert_eq!(api_req.duration, "60");
assert_eq!(api_req.start, "2024-01-01:00-00");
}
#[test]
fn timeshift_path_request_prefers_query_when_form_is_missing() {
let timeshift_request = XtreamApiTimeShiftRequest {
username: String::new(),
password: String::new(),
duration: String::new(),
start: String::new(),
stream_id: String::new(),
};
let api_query_req = UserApiRequest {
username: String::from("query-user"),
password: String::from("query-pass"),
stream_id: String::from("42"),
duration: String::from("60"),
start: String::from("2024-01-01:00-00"),
..UserApiRequest::default()
};
let query_req = UserApiRequest::merge_query_over_form(&api_query_req, None);
let path_req = UserApiRequest {
username: timeshift_request.username,
password: timeshift_request.password,
duration: timeshift_request.duration,
start: timeshift_request.start,
stream_id: timeshift_request.stream_id,
..UserApiRequest::default()
};
let api_req = UserApiRequest::merge_prefer_primary(&path_req, &query_req);
assert_eq!(api_req.username, "query-user");
assert_eq!(api_req.password, "query-pass");
assert_eq!(api_req.stream_id, "42");
assert_eq!(api_req.duration, "60");
assert_eq!(api_req.start, "2024-01-01:00-00");
}
#[test]
fn timeshift_path_request_prefers_path_over_query_and_form() {
let timeshift_request = XtreamApiTimeShiftRequest {
username: String::from("path-user"),
password: String::from("path-pass"),
duration: String::from("120"),
start: String::from("path-start"),
stream_id: String::from("7"),
};
let api_query_req = UserApiRequest {
username: String::from("query-user"),
password: String::from("query-pass"),
stream_id: String::from("42"),
duration: String::from("60"),
start: String::from("query-start"),
..UserApiRequest::default()
};
let form_req = UserApiRequest {
username: String::from("form-user"),
password: String::from("form-pass"),
stream_id: String::from("99"),
duration: String::from("10"),
start: String::from("form-start"),
..UserApiRequest::default()
};
let query_req = UserApiRequest::merge_query_over_form(&api_query_req, Some(&form_req));
let path_req = UserApiRequest {
username: timeshift_request.username,
password: timeshift_request.password,
duration: timeshift_request.duration,
start: timeshift_request.start,
stream_id: timeshift_request.stream_id,
..UserApiRequest::default()
};
let api_req = UserApiRequest::merge_prefer_primary(&path_req, &query_req);
assert_eq!(api_req.username, "path-user");
assert_eq!(api_req.password, "path-pass");
assert_eq!(api_req.stream_id, "7");
assert_eq!(api_req.duration, "120");
assert_eq!(api_req.start, "path-start");
}
}