Files
tuliprox/src/api/xtream_api.rs
T

595 lines
27 KiB
Rust
Raw Normal View History

// https://github.com/tellytv/go.xtream-codes/blob/master/structs.go
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-05-06 10:22:35 +02:00
use std::io::{Error, ErrorKind};
2024-03-26 10:26:12 +01:00
use std::path::Path;
2023-11-22 21:06:58 +01:00
use std::str::FromStr;
2024-05-06 10:22:35 +02:00
2024-05-02 15:22:23 +02:00
use actix_web::{HttpRequest, HttpResponse, web};
use bytes::Bytes;
2024-11-02 08:53:20 +01:00
use futures::Stream;
2024-11-01 19:38:08 +01:00
use futures::stream::{self, StreamExt};
2024-12-03 14:49:23 +01:00
use log::{debug, error, log_enabled, Level};
2024-05-06 10:22:35 +02:00
use serde_json::{Map, Value};
use crate::api::api_model::{AppState, UserApiRequest, XtreamAuthorizationResponse};
2024-05-06 10:22:35 +02:00
use crate::api::api_utils::{get_user_server_info, get_user_target, get_user_target_by_credentials, serve_file, stream_response};
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
2024-05-03 01:47:41 +02:00
use crate::model::api_proxy::{ProxyType, ProxyUserCredentials};
2024-04-01 11:59:54 +02:00
use crate::model::config::{Config, ConfigInput, ConfigTarget};
2024-05-06 10:22:35 +02:00
use crate::model::config::TargetType;
use crate::model::playlist::{PlaylistItemType, XtreamCluster, XtreamPlaylistItem};
2024-05-06 10:22:35 +02:00
use crate::model::xtream::XtreamMappingOptions;
use crate::repository::storage::{get_target_storage_path, hash_string};
use crate::repository::target_id_mapping::TargetIdMapping;
2024-03-26 10:26:12 +01:00
use crate::repository::xtream_repository;
use crate::utils::{json_utils, request_utils};
use crate::utils::request_utils::mask_sensitive_info;
2024-03-26 10:26:12 +01:00
const ACTION_GET_SERIES_INFO: &str = "get_series_info";
const ACTION_GET_VOD_INFO: &str = "get_vod_info";
const ACTION_GET_LIVE_INFO: &str = "get_live_info";
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 ACTION_GET_LIVE_CATEGORIES: &str = "get_live_categories";
const ACTION_GET_VOD_CATEGORIES: &str = "get_vod_categories";
const ACTION_GET_SERIES_CATEGORIES: &str = "get_series_categories";
const ACTION_GET_LIVE_STREAMS: &str = "get_live_streams";
const ACTION_GET_VOD_STREAMS: &str = "get_vod_streams";
const ACTION_GET_SERIES: &str = "get_series";
const TAG_ID: &str = "id";
const TAG_CATEGORY_ID: &str = "category_id";
const TAG_DIRECT_SOURCE: &str = "direct_source";
const TAG_STREAM_ID: &str = "stream_id";
const TAG_MOVIE_DATA: &str = "movie_data";
const TAG_EPG_LISTINGS: &str = "epg_listings";
macro_rules! try_option_bad_request {
($option:expr, $msg_is_error:expr, $msg:expr) => {
match $option {
Some(value) => value,
None => {
if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);}
return HttpResponse::BadRequest().finish();
}
}
};
($option:expr) => {
match $option {
Some(value) => value,
None => return HttpResponse::BadRequest().finish(),
}
};
}
macro_rules! try_result_bad_request {
($option:expr, $msg_is_error:expr, $msg:expr) => {
match $option {
Ok(value) => value,
Err(_) => {
if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);}
return HttpResponse::BadRequest().finish();
}
}
};
($option:expr) => {
match $option {
Ok(value) => value,
Err(_) => return HttpResponse::BadRequest().finish(),
}
};
}
2024-05-10 10:13:50 +02:00
enum XtreamApiStreamContext {
LiveAlt,
Live,
Movie,
Series,
Timeshift,
}
impl XtreamApiStreamContext {
const LIVE_ALT: &'static str = "";
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 {
2024-11-04 18:46:56 +01:00
Self::LiveAlt => Self::LIVE_ALT,
Self::Live => Self::LIVE,
Self::Movie => Self::MOVIE,
Self::Series => Self::SERIES,
Self::Timeshift => Self::TIMESHIFT,
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-05-10 10:13:50 +02:00
username: &'a str,
password: &'a str,
stream_id: &'a str,
action_path: &'a str) -> Self {
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
fn get_xtream_player_api_action_url(input: &ConfigInput, action: &str) -> Option<String> {
2024-04-01 11:59:54 +02:00
if let Some(user_info) = input.get_user_info() {
Some(format!("{}/player_api.php?username={}&password={}&action={}",
&user_info.base_url,
&user_info.username,
&user_info.password,
action
))
} else {
None
2023-12-01 21:38:23 +01:00
}
}
fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: XtreamCluster, stream_id: u32) -> Option<String> {
let (action, stream_id_field) = match cluster {
XtreamCluster::Live => (ACTION_GET_LIVE_INFO, "live_id"),
XtreamCluster::Video => (ACTION_GET_VOD_INFO, "vod_id"),
XtreamCluster::Series => (ACTION_GET_SERIES_INFO, "series_id"),
};
2024-05-10 12:01:48 +02:00
get_xtream_player_api_action_url(input, action).map(|action_url| format!("{action_url}&{stream_id_field}={stream_id}"))
}
fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &str, action_path: &str, fallback_url: &str) -> Option<String> {
2024-05-10 12:01:48 +02:00
let ctx_path = if context.is_empty() { String::new() } else { format!("{context}/") };
2024-04-01 11:59:54 +02:00
if let Some(user_info) = input.get_user_info() {
Some(format!("{}/{}{}/{}/{}",
&user_info.base_url,
ctx_path,
&user_info.username,
&user_info.password,
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
}
}
2024-05-03 01:47:41 +02:00
fn get_user_info(user: &ProxyUserCredentials, cfg: &Config) -> XtreamAuthorizationResponse {
let server_info = get_user_server_info(cfg, user);
XtreamAuthorizationResponse::new(&server_info, user)
}
2024-05-05 19:15:54 +02:00
fn xtream_api_request_separate_number_and_rest(input: &str) -> (String, String) {
2024-11-04 18:46:56 +01:00
input.find('.').map_or_else(|| (input.to_string(), String::new()), |dot_index| {
2024-03-19 10:25:03 +01:00
let number_part = input[..dot_index].to_string();
let rest = input[dot_index..].to_string();
(number_part, rest)
2024-11-04 18:46:56 +01:00
})
2024-03-19 10:20:06 +01: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 {
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();
}
let (action_stream_id, stream_ext) = xtream_api_request_separate_number_and_rest(stream_req.stream_id);
let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse());
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));
let input = try_option_bad_request!(app_state.config.get_input_by_id(pli.input_id), true, format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context));
2024-12-02 17:24:06 +01:00
if pli.item_type == PlaylistItemType::LiveHls {
let stream_url = pli.url.to_string();
2024-12-03 14:49:23 +01:00
if log_enabled!(Level::Debug) {
debug!("Redirecting stream request to {}", mask_sensitive_info(&stream_url));
}
2024-12-02 17:24:06 +01:00
return HttpResponse::Found().insert_header(("Location", stream_url)).finish();
}
let query_path = if stream_req.action_path.is_empty() {
format!("{}{stream_ext}", pli.provider_id)
2023-12-05 17:54:42 +01:00
} else {
format!("{}/{}{stream_ext}", stream_req.action_path, pli.provider_id)
};
if user.proxy == ProxyType::Redirect {
2024-12-03 14:49:23 +01:00
if log_enabled!(Level::Debug) {
debug!("Redirecting stream request to {}", mask_sensitive_info(&pli.url));
}
return HttpResponse::Found().insert_header(("Location", mask_sensitive_info(pli.url.as_str()))).finish();
2023-11-22 21:06:58 +01:00
}
2024-12-03 14:49:23 +01:00
let stream_url = try_option_bad_request!(get_xtream_player_api_stream_url(input, stream_req.context.to_string().as_str(), &query_path, pli.url.as_str()), true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context));
if log_enabled!(Level::Debug) {
debug!("Streaming stream request from {}", mask_sensitive_info(&stream_url));
}
stream_response(&stream_url, req, Some(input)).await
2023-11-22 21:06:58 +01:00
}
2023-11-22 21:06:58 +01:00
async fn xtream_player_api_live_stream(
2023-12-01 21:38:23 +01:00
req: HttpRequest,
2023-11-22 21:06:58 +01:00
api_req: web::Query<UserApiRequest>,
path: web::Path<(String, String, String)>,
2024-05-05 19:15:54 +02:00
app_state: web::Data<AppState>,
2023-11-22 21:06:58 +01:00
) -> HttpResponse {
let (username, password, stream_id) = path.into_inner();
2024-05-06 10:22:35 +02:00
xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Live, &username, &password, &stream_id, "")).await
2023-11-22 21:06:58 +01:00
}
async fn xtream_player_api_live_stream_alt(
req: HttpRequest,
api_req: web::Query<UserApiRequest>,
path: web::Path<(String, String, String)>,
2024-05-05 19:15:54 +02:00
app_state: web::Data<AppState>,
) -> HttpResponse {
let (username, password, stream_id) = path.into_inner();
2024-05-08 20:42:15 +02:00
xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::LiveAlt, &username, &password, &stream_id, "")).await
}
2023-11-22 21:06:58 +01:00
async fn xtream_player_api_series_stream(
2023-12-01 21:38:23 +01:00
req: HttpRequest,
2023-11-22 21:06:58 +01:00
api_req: web::Query<UserApiRequest>,
path: web::Path<(String, String, String)>,
2024-05-05 19:15:54 +02:00
app_state: web::Data<AppState>,
2023-11-22 21:06:58 +01:00
) -> HttpResponse {
let (username, password, stream_id) = path.into_inner();
2024-05-08 20:42:15 +02:00
xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Series, &username, &password, &stream_id, "")).await
2023-11-22 21:06:58 +01:00
}
async fn xtream_player_api_movie_stream(
2023-12-01 21:38:23 +01:00
req: HttpRequest,
2023-11-22 21:06:58 +01:00
api_req: web::Query<UserApiRequest>,
path: web::Path<(String, String, String)>,
2024-05-05 19:15:54 +02:00
app_state: web::Data<AppState>,
2023-11-22 21:06:58 +01:00
) -> HttpResponse {
let (username, password, stream_id) = path.into_inner();
2024-05-08 20:42:15 +02:00
xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Movie, &username, &password, &stream_id, "")).await
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,
2023-11-24 22:53:33 +01:00
api_req: web::Query<UserApiRequest>,
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 {
let (username, password, duration, start, stream_id) = path.into_inner();
2024-05-24 15:10:29 +02:00
let action_path = format!("{duration}/{start}");
2024-05-08 20:42:15 +02:00
xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Timeshift, &username, &password, &stream_id, &action_path)).await
2024-03-18 17:30:52 +01:00
}
2024-09-03 11:58:11 +02:00
async fn xtream_player_api_streaming_timeshift(
req: HttpRequest,
api_req: web::Query<UserApiRequest>,
app_state: web::Data<AppState>,
) -> HttpResponse {
let username = &api_req.username;
let password = &api_req.password;
let stream_id = &api_req.stream;
let duration = &api_req.duration;
let start = &api_req.start;
let action_path = format!("{duration}/{start}");
xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Timeshift, username, password, stream_id, &action_path)).await
}
2024-05-07 17:13:22 +02:00
fn get_xtream_vod_info(target: &ConfigTarget, pli: &XtreamPlaylistItem, content: &str) -> Result<String, Error> {
if let Ok(mut doc) = serde_json::from_str::<Map<String, Value>>(content) {
if let Some(Value::Object(movie_data)) = doc.get_mut(TAG_MOVIE_DATA) {
2024-10-25 00:39:22 +02:00
let stream_id = pli.virtual_id;
2024-05-07 17:13:22 +02:00
let category_id = pli.category_id;
movie_data.insert(TAG_STREAM_ID.to_string(), Value::Number(serde_json::value::Number::from(stream_id)));
movie_data.insert(TAG_CATEGORY_ID.to_string(), Value::Number(serde_json::value::Number::from(category_id)));
2024-05-07 17:13:22 +02:00
let options = XtreamMappingOptions::from_target_options(target.options.as_ref());
if options.skip_video_direct_source {
movie_data.insert(TAG_DIRECT_SOURCE.to_string(), Value::String(String::new()));
2024-05-07 17:13:22 +02:00
} else {
movie_data.insert(TAG_DIRECT_SOURCE.to_string(), Value::String(pli.url.to_string()));
2024-05-07 17:13:22 +02:00
}
if let Ok(result) = serde_json::to_string(&doc) {
return Ok(result);
}
}
}
2024-10-25 00:39:22 +02:00
Err(Error::new(ErrorKind::Other, format!("Failed to get vod info for id {}", pli.virtual_id)))
2024-05-07 17:13:22 +02:00
}
async fn xtream_get_stream_info_content(info_url: &str, input: &ConfigInput) -> Result<String, Error> {
2024-05-27 09:51:35 +02:00
request_utils::download_text_content(input, info_url, None).await
}
async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &ConfigTarget,
pli: &XtreamPlaylistItem, info_url: &str, cluster: XtreamCluster) -> Result<String, Error> {
if cluster == XtreamCluster::Series {
if let Some(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.virtual_id) {
return Ok(content);
}
}
if let Ok(content) = xtream_get_stream_info_content(info_url, input).await {
return match cluster {
XtreamCluster::Live => Ok(content),
2024-10-25 18:26:25 +02:00
XtreamCluster::Video => get_xtream_vod_info(target, pli, &content),
2024-10-28 20:15:27 +01:00
XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, pli, &content),
};
}
Err(Error::new(ErrorKind::Other, format!("Cant find stream with id: {}/{}/{}",
2024-10-25 00:39:22 +02:00
target.name.replace(' ', "_").as_str(), &cluster, pli.virtual_id)))
2023-12-01 21:38:23 +01: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,
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()
};
2024-10-25 18:26:25 +02:00
if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(cluster)) {
2024-05-06 18:47:56 +02:00
let input_id = pli.input_id;
2024-05-10 12:01:48 +02:00
if let Some(input) = app_state.config.get_input_by_id(input_id) {
2024-05-08 20:42:15 +02:00
if let Some(info_url) = get_xtream_player_api_info_url(input, cluster, pli.provider_id) {
2024-05-07 17:13:22 +02:00
// Redirect is only possible for live streams, vod and series info needs to be modified
if user.proxy == ProxyType::Redirect && cluster == XtreamCluster::Live {
2024-04-01 11:59:54 +02:00
return HttpResponse::Found().insert_header(("Location", info_url)).finish();
2024-05-08 20:42:15 +02:00
} else if let Ok(content) = xtream_get_stream_info(&app_state.config, input, target, &pli, info_url.as_str(), cluster).await {
2024-05-06 10:22:35 +02:00
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content);
2024-04-01 11:59:54 +02:00
}
}
}
}
2024-05-07 17:13:22 +02:00
match cluster {
XtreamCluster::Live => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{}"),
XtreamCluster::Video => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{info:[]}"),
XtreamCluster::Series => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("[]"),
}
}
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
2024-10-28 20:15:27 +01:00
if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None) {
2024-05-06 18:47:56 +02:00
let input_id: u16 = pli.input_id;
2024-05-10 12:01:48 +02:00
if let Some(input) = app_state.config.get_input_by_id(input_id) {
if let Some(action_url) = get_xtream_player_api_action_url(input, ACTION_GET_SHORT_EPG) {
let mut info_url = format!("{action_url}&{TAG_STREAM_ID}={}", pli.provider_id);
2024-05-06 10:22:35 +02:00
if !(limit.is_empty() || limit.eq("0")) {
2024-05-10 12:01:48 +02:00
info_url = format!("{info_url}&limit={limit}");
2024-05-06 10:22:35 +02:00
}
2024-05-27 09:51:35 +02:00
if user.proxy == ProxyType::Redirect {
return HttpResponse::Found().insert_header(("Location", info_url)).finish();
}
2024-05-06 10:22:35 +02:00
2024-05-27 09:51:35 +02:00
return match request_utils::download_text_content(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 {}", mask_sensitive_info(err.to_string().as_str()));
2024-05-27 09:51:35 +02:00
HttpResponse::NoContent().finish()
2024-05-06 10:22:35 +02:00
}
2024-05-27 09:51:35 +02:00
};
2024-05-06 10:22:35 +02:00
}
}
}
}
error!("Cant find short epg with id: {target_name}/{stream_id}");
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 {
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),
_ => Err(Error::new(ErrorKind::Other, ""))
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() {
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
}
async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget, stream_id: &str, start: &str, end: &str) -> HttpResponse {
let virtual_id: u32 = try_result_bad_request!(FromStr::from_str(stream_id));
let pli = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(XtreamCluster::Live)));
let input = try_option_bad_request!(app_state.config.get_input_by_id(pli.input_id));
let info_url = try_option_bad_request!(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_stream_info_content(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()));
let mut target_id_mapping = TargetIdMapping::new(&target_path);
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()) {
let uuid = hash_string(&format!("{}/{}", pli.url, catchup_provider_id));
let virtual_id = target_id_mapping.insert_entry(uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id);
epg_list_item.insert(TAG_ID.to_string(), Value::String(virtual_id.to_string()));
}
}
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))
}
2023-11-02 20:03:22 +01:00
async fn xtream_player_api(
req: &HttpRequest,
api_req: UserApiRequest,
2024-05-10 12:01:48 +02:00
app_state: &web::Data<AppState>,
) -> HttpResponse {
2024-11-21 18:11:25 +01:00
let user_target = get_user_target(&api_req, app_state);
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));
}
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));
}
match action {
ACTION_GET_SERIES_INFO => {
return xtream_get_stream_info_response(
app_state, &user, target, api_req.series_id.trim(), XtreamCluster::Series
).await;
}
ACTION_GET_VOD_INFO => {
return xtream_get_stream_info_response(
app_state, &user, target, api_req.vod_id.trim(), XtreamCluster::Video
).await;
}
ACTION_GET_EPG | ACTION_GET_SHORT_EPG => {
return xtream_get_short_epg(
app_state, &user, target, api_req.stream_id.trim(), api_req.limit.trim()
).await;
}
ACTION_GET_CATCHUP_TABLE => {
return 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
2024-11-02 08:53:20 +01:00
// Handle general content actions
if let Some(response) = xtream_player_api_handle_content_action(
&app_state.config, &target.name, action, api_req.category_id.trim(), req
).await {
return response;
}
// Process specific playlist actions
let category_id = api_req.category_id.trim().parse::<u32>().unwrap_or(0);
let result = match action {
ACTION_GET_LIVE_STREAMS => xtream_repository::xtream_load_rewrite_playlist(
XtreamCluster::Live, &app_state.config, target, category_id
),
ACTION_GET_VOD_STREAMS => xtream_repository::xtream_load_rewrite_playlist(
XtreamCluster::Video, &app_state.config, target, category_id
),
ACTION_GET_SERIES => xtream_repository::xtream_load_rewrite_playlist(
XtreamCluster::Series, &app_state.config, target, category_id
),
_ => Err(M3uFilterError::new(M3uFilterErrorKind::Info,format!("Cant find action: {action} for target: {}", &target.name)
)),
};
match result {
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);
HttpResponse::NoContent().finish()
}
}
} else {
2024-11-21 18:11:25 +01:00
if user_target.is_none() {
debug!("Cant find user!");
} else if api_req.action.is_empty() {
debug!("Paremeter action is empty!");
} else {
debug!("Bad request!" );
}
HttpResponse::BadRequest().finish()
}
}
2023-11-02 20:03:22 +01:00
2024-11-02 08:53:20 +01:00
fn xtream_create_content_stream(xtream_iter: impl Iterator<Item = String>) -> impl Stream<Item = Result<Bytes, String>> {
let mut first_item = true;
stream::iter(xtream_iter.map(move |line| {
let line = if first_item {
first_item = false;
format!("[{line}")
} else {
format!(",{line}")
};
Ok::<Bytes, String>(Bytes::from(line))
}))
.chain(stream::once(async { Ok::<Bytes, String>(Bytes::from("]")) }))
}
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>,
) -> HttpResponse {
2024-05-05 19:15:54 +02:00
xtream_player_api(&req, api_req.into_inner(), &app_state).await
}
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>,
) -> HttpResponse {
2024-05-05 19:15:54 +02:00
xtream_player_api(&req, api_req.into_inner(), &app_state).await
}
2024-11-04 18:46:56 +01:00
pub fn xtream_api_register(cfg: &mut web::ServiceConfig) {
2024-11-21 18:11:25 +01:00
cfg.service(web::resource("/player_api.php").route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post)))
.service(web::resource("/panel_api.php").route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post)))
2024-05-06 10:22:35 +02:00
.service(web::resource("/xtream").route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post)))
.service(web::resource("/{username}/{password}/{stream_id}").route(web::get().to(xtream_player_api_live_stream_alt)))
.service(web::resource("/live/{username}/{password}/{stream_id}").route(web::get().to(xtream_player_api_live_stream)))
.service(web::resource("/movie/{username}/{password}/{stream_id}").route(web::get().to(xtream_player_api_movie_stream)))
.service(web::resource("/series/{username}/{password}/{stream_id}").route(web::get().to(xtream_player_api_series_stream)))
2024-09-03 11:58:11 +02:00
.service(web::resource("/timeshift/{username}/{password}/{duration}/{start}/{stream_id}").route(web::get().to(xtream_player_api_timeshift_stream)))
.service(web::resource("/timeshift.php").route(web::get().to(xtream_player_api_streaming_timeshift)))
.service(web::resource("/streaming/timeshift.php").route(web::get().to(xtream_player_api_streaming_timeshift)));
2024-05-02 15:22:23 +02:00
/* TODO
cfg.service(web::resource("/hlsr/{token}/{username}/{password}/{channel}/{hash}/{chunk}").route(web::get().to(xtream_player_api_hlsr_stream)));
cfg.service(web::resource("/hls/{token}/{chunk}").route(web::get().to(xtream_player_api_hls_stream)));
cfg.service(web::resource("/play/{token}/{type}").route(web::get().to(xtream_player_api_play_stream)));
*/
2023-11-02 20:03:22 +01:00
}