Files
tuliprox/src/api/endpoints/xtream_api.rs
T

798 lines
38 KiB
Rust
Raw Normal View History

// 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};
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;
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};
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;
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};
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
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,
}
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
))
} 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;
XtreamAuthorizationResponse::new(&server_info, user, active_connections, app_state.config.user_access_control)
}
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();
}
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-31 23:08:23 +01:00
let (action_stream_id, stream_ext) = separate_number_and_remainder(stream_req.stream_id);
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));
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
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
}
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));
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()),
true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}",
stream_req.context));
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)
}
async fn xtream_player_api_resource(
2025-03-11 14:56:10 +01:00
req_headers: &HeaderMap,
api_req: &UserApiRequest,
app_state: &Arc<AppState>,
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
}
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();
}
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)
};
match stream_url {
2025-03-11 14:56:10 +01:00
None => axum::http::StatusCode::NOT_FOUND.into_response(),
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()
} 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-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
}
}
}
2025-03-11 14:56:10 +01:00
macro_rules! create_xtream_player_api_stream {
($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()
}
}
}
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
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
};
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
}
}
}
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
};
}
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-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
}
}
}
}
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 {
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 {
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);
let mut doc: Map<String, Value> = try_result_bad_request!(serde_json::from_str(&content));
let epg_listings = try_option_bad_request!(doc.get_mut(TAG_EPG_LISTINGS).and_then(Value::as_array_mut));
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;
for epg_list_item in epg_listings.iter_mut().filter_map(Value::as_object_mut) {
// TODO epg_id
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);
epg_list_item.insert(TAG_ID.to_string(), Value::String(virtual_id.to_string()));
}
}
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())
}
2025-01-31 23:08:23 +01:00
macro_rules! skip_json_response_if_flag_set {
($flag:expr, $stmt:expr) => {
if $flag {
2025-03-11 14:56:10 +01:00
return api_utils::empty_json_list_response().into_response();
}
2025-03-11 14:56:10 +01:00
return $stmt.into_response();
};
}
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(
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
}
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
}
// 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-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 {
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),
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),
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 {
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()
}
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-11-02 08:53:20 +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()
}
}
} 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-11-02 20:03:22 +01:00
fn xtream_create_content_stream(xtream_iter: impl Iterator<Item=(String, bool)>) -> impl Stream<Item=Result<Bytes, String>> {
stream::once(async { Ok::<Bytes, String>(Bytes::from("[")) }).chain(
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 {
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
}
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
)*
}};
}
macro_rules! register_xtream_api_resource {
2025-03-11 14:56:10 +01:00
($router:expr, [$(($path:expr, $fn_name:ident)),*]) => {{
$router
$(
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)));
)*
}};
}
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, [
("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
}