Files
tuliprox/backend/src/api/endpoints/m3u_api.rs
T

244 lines
12 KiB
Rust
Raw Normal View History

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};
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 {
match get_user_target(api_req, app_state) {
Some((user, target)) => {
2024-12-05 19:54:34 +01:00
match m3u_load_rewrite_playlist(&app_state.config, target, &user).await {
Ok(m3u_iter) => {
// Convert the iterator into a stream of `Bytes`
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());
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-03-11 14:56:10 +01:00
builder.body(axum::body::Body::from_stream(content_stream)).unwrap().into_response()
}
Err(err) => {
error!("{}", sanitize_sensitive_info(err.to_string().as_str()));
2025-03-11 14:56:10 +01:00
axum::http::StatusCode::NO_CONTENT.into_response()
}
}
}
2025-03-11 14:56:10 +01:00
None => axum::http::StatusCode::BAD_REQUEST.into_response(),
}
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
}
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) {
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-06-04 21:40:14 +02:00
let session_url = if let Some(session) = &user_session {
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-22 17:08:31 +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-06-05 08:16:52 +02:00
if session.virtual_id == virtual_id && is_seek_request(cluster, req_headers).await {
// 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-06-04 21:40:14 +02:00
session.stream_url.as_str()
} else {
pli.url.as_str()
};
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-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
}
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() };
let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state)
else { return StatusCode::BAD_REQUEST.into_response() };
2025-03-16 17:08:00 +01:00
if user.permission_denied(&app_state) {
return StatusCode::FORBIDDEN.into_response();
2025-02-06 20:31:51 +01:00
}
let target_name = &target.name;
if !target.has_output(&TargetType::M3u) {
debug!("Target has no m3u playlist {target_name}");
return StatusCode::BAD_REQUEST.into_response();
}
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 {
Ok(item) => item,
Err(err) => {
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();
}
};
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(),
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) {
debug!("Redirecting stream request to {}", sanitize_sensitive_info(&url));
2025-05-23 00:27:35 +02:00
redirect(&url).into_response()
} else {
2025-05-23 00:27:35 +02:00
resource_response(&app_state, &url, &req_headers, None).await.into_response()
}
}
}
}
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
)*
}};
}
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))
}