2023-10-09 00:17:54 +02:00
|
|
|
// 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};
|
2023-10-09 00:17:54 +02:00
|
|
|
use chrono::{Duration, Local};
|
2023-12-01 21:38:23 +01:00
|
|
|
use log::{debug, error};
|
2024-05-06 10:22:35 +02:00
|
|
|
use serde_json::{Map, Value};
|
2024-03-28 12:30:05 +01:00
|
|
|
use url::Url;
|
2023-10-20 12:18:44 +02:00
|
|
|
|
2023-10-25 09:57:44 +02:00
|
|
|
use crate::api::api_model::{AppState, UserApiRequest, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo};
|
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};
|
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;
|
2024-05-06 18:47:56 +02:00
|
|
|
use crate::model::playlist::{XtreamCluster, XtreamPlaylistItem};
|
2024-05-06 10:22:35 +02:00
|
|
|
use crate::model::xtream::XtreamMappingOptions;
|
2024-03-26 10:26:12 +01:00
|
|
|
use crate::repository::xtream_repository;
|
2024-05-05 19:15:54 +02:00
|
|
|
use crate::repository::xtream_repository::get_xtream_item_for_stream_id;
|
2024-03-26 10:32:04 +01:00
|
|
|
use crate::utils::{json_utils, request_utils};
|
2024-03-26 10:26:12 +01:00
|
|
|
|
|
|
|
|
pub(crate) async fn serve_query(file_path: &Path, filter: &HashMap<&str, &str>) -> HttpResponse {
|
2024-03-26 10:32:04 +01:00
|
|
|
let filtered = json_utils::filter_json_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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 10:22:35 +02:00
|
|
|
fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: &XtreamCluster, stream_id: u32) -> Option<String> {
|
2023-12-02 20:47:26 +01:00
|
|
|
let (action, stream_id_field) = match cluster {
|
|
|
|
|
XtreamCluster::Live => ("get_live_info", "live_id"),
|
|
|
|
|
XtreamCluster::Video => ("get_vod_info", "vod_id"),
|
|
|
|
|
XtreamCluster::Series => ("get_series_info", "series_id"),
|
|
|
|
|
};
|
|
|
|
|
get_xtream_player_api_action_url(input, action).map(|action_url| format!("{}&{}={}", action_url, stream_id_field, stream_id))
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-01 21:38:23 +01:00
|
|
|
fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &str, action_path: &str) -> Option<String> {
|
2023-12-02 20:47:26 +01:00
|
|
|
let ctx_path = if context.is_empty() { "".to_string() } 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
|
|
|
))
|
2024-04-01 11:59:54 +02:00
|
|
|
} else {
|
|
|
|
|
None
|
2023-12-01 21:38:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-09 00:17:54 +02:00
|
|
|
|
2024-05-03 01:47:41 +02:00
|
|
|
fn get_user_info(user: &ProxyUserCredentials, cfg: &Config) -> XtreamAuthorizationResponse {
|
2024-04-01 16:49:12 +02:00
|
|
|
let server_info = get_user_server_info(cfg, user);
|
2024-01-29 12:10:29 +01:00
|
|
|
|
2023-10-09 00:17:54 +02:00
|
|
|
let now = Local::now();
|
|
|
|
|
XtreamAuthorizationResponse {
|
|
|
|
|
user_info: XtreamUserInfo {
|
2023-10-26 19:50:35 +02:00
|
|
|
active_cons: "0".to_string(),
|
2023-10-09 00:17:54 +02:00
|
|
|
allowed_output_formats: Vec::from(["ts".to_string()]),
|
|
|
|
|
auth: 1,
|
2023-10-09 18:18:27 +02:00
|
|
|
created_at: (now - Duration::days(365)).timestamp(), // fake
|
|
|
|
|
exp_date: (now + Duration::days(365)).timestamp(),// fake
|
2023-10-26 19:50:35 +02:00
|
|
|
is_trial: "0".to_string(),
|
|
|
|
|
max_connections: "1".to_string(),
|
2024-01-29 12:10:29 +01:00
|
|
|
message: server_info.message.to_string(),
|
2024-04-22 10:28:14 +02:00
|
|
|
password: user.password.to_string(),
|
|
|
|
|
username: user.username.to_string(),
|
2023-10-09 00:17:54 +02:00
|
|
|
status: "Active".to_string(),
|
|
|
|
|
},
|
|
|
|
|
server_info: XtreamServerInfo {
|
2024-01-29 12:10:29 +01:00
|
|
|
url: server_info.host.to_owned(),
|
|
|
|
|
port: server_info.http_port.to_owned(),
|
|
|
|
|
https_port: server_info.https_port.to_owned(),
|
|
|
|
|
server_protocol: server_info.protocol.clone(),
|
|
|
|
|
rtmp_port: server_info.rtmp_port.to_owned(),
|
|
|
|
|
timezone: server_info.timezone.to_string(),
|
2023-10-09 00:17:54 +02:00
|
|
|
timestamp_now: now.timestamp(),
|
|
|
|
|
time_now: now.format("%Y-%m-%d %H:%M:%S").to_string(),
|
2023-10-09 18:18:27 +02:00
|
|
|
},
|
2023-10-09 00:17:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-05 19:15:54 +02:00
|
|
|
fn xtream_api_request_separate_number_and_rest(input: &str) -> (String, String) {
|
2024-03-19 10:25:03 +01:00
|
|
|
if let Some(dot_index) = input.find('.') {
|
|
|
|
|
let number_part = input[..dot_index].to_string();
|
|
|
|
|
let rest = input[dot_index..].to_string();
|
|
|
|
|
(number_part, rest)
|
|
|
|
|
} else {
|
|
|
|
|
(input.to_string(), String::new())
|
|
|
|
|
}
|
2024-03-19 10:20:06 +01:00
|
|
|
}
|
|
|
|
|
|
2024-05-05 19:15:54 +02:00
|
|
|
enum XtreamApiStreamContext {
|
|
|
|
|
LiveAlt,
|
|
|
|
|
Live,
|
|
|
|
|
Movie,
|
|
|
|
|
Series,
|
2024-05-06 10:22:35 +02:00
|
|
|
Timeshift,
|
2024-05-05 19:15:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Display for XtreamApiStreamContext {
|
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
write!(f, "{}", match self {
|
|
|
|
|
XtreamApiStreamContext::LiveAlt => "",
|
|
|
|
|
XtreamApiStreamContext::Live => "live",
|
|
|
|
|
XtreamApiStreamContext::Movie => "movie",
|
|
|
|
|
XtreamApiStreamContext::Series => "series",
|
2024-05-06 10:22:35 +02:00
|
|
|
XtreamApiStreamContext::Timeshift => "timeshift",
|
2024-05-05 19:15:54 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 10:22:35 +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> {
|
|
|
|
|
pub fn from(context: XtreamApiStreamContext,
|
|
|
|
|
username: &'a str,
|
|
|
|
|
password: &'a str,
|
|
|
|
|
stream_id: &'a str,
|
|
|
|
|
action_path: &'a str) -> Self {
|
|
|
|
|
XtreamApiStreamRequest {
|
|
|
|
|
context,
|
|
|
|
|
username,
|
|
|
|
|
password,
|
|
|
|
|
stream_id,
|
|
|
|
|
action_path,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2024-05-06 10:22:35 +02:00
|
|
|
if let Some((user, target)) = get_user_target_by_credentials(stream_req.username, stream_req.password, api_req, app_state) {
|
2023-11-22 21:06:58 +01:00
|
|
|
let target_name = &target.name;
|
|
|
|
|
if target.has_output(&TargetType::Xtream) {
|
2024-05-06 10:22:35 +02:00
|
|
|
let (action_stream_id, stream_ext) = xtream_api_request_separate_number_and_rest(stream_req.stream_id);
|
2024-05-05 19:15:54 +02:00
|
|
|
let req_stream_id: u32 = match FromStr::from_str(action_stream_id.trim()) {
|
|
|
|
|
Ok(id) => id,
|
|
|
|
|
Err(_) => return HttpResponse::BadRequest().finish()
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-06 18:55:28 +02:00
|
|
|
match get_xtream_item_for_stream_id(req_stream_id, &app_state.config, target, None) {
|
2024-05-05 19:15:54 +02:00
|
|
|
Ok(pli) => {
|
2024-05-06 18:47:56 +02:00
|
|
|
let input_id: u16 = pli.input_id;
|
2024-05-06 10:22:35 +02:00
|
|
|
if let Some(input) = app_state.config.get_input_by_id(&input_id) {
|
|
|
|
|
let mut query_path = if stream_req.action_path.is_empty() { "".to_string() } else { format!("{}/", stream_req.action_path) };
|
2024-05-06 18:47:56 +02:00
|
|
|
query_path = format!("{}{}{}", query_path, pli.id, stream_ext);
|
2024-05-06 10:22:35 +02:00
|
|
|
if let Some(stream_url) = get_xtream_player_api_stream_url(input, stream_req.context.to_string().as_str(), query_path.as_str()) {
|
2024-05-05 19:15:54 +02:00
|
|
|
if user.proxy == ProxyType::Redirect {
|
|
|
|
|
debug!("Redirecting stream request to {}", stream_url);
|
|
|
|
|
return HttpResponse::Found().insert_header(("Location", stream_url)).finish();
|
|
|
|
|
}
|
2024-05-06 10:22:35 +02:00
|
|
|
return stream_response(&stream_url, req, Some(input)).await;
|
2024-05-05 19:15:54 +02:00
|
|
|
} else {
|
2024-05-06 10:22:35 +02:00
|
|
|
error!("Cant find stream url for target {}, context {}, stream_id {}", target_name, stream_req.context, req_stream_id);
|
2024-05-05 19:15:54 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2024-05-06 10:22:35 +02:00
|
|
|
error!("Cant find input for target {}, context {}, stream_id {}", target_name, stream_req.context, req_stream_id);
|
2023-12-02 20:47:26 +01:00
|
|
|
}
|
2024-05-06 10:22:35 +02:00
|
|
|
}
|
2024-05-05 19:15:54 +02:00
|
|
|
Err(_) => error!("Failed to read xtream item for stream id {}", req_stream_id),
|
2023-11-22 21:06:58 +01:00
|
|
|
}
|
2023-12-05 17:54:42 +01:00
|
|
|
} else {
|
|
|
|
|
debug!("Target has no xtream output {}", target_name);
|
2023-11-22 21:06:58 +01:00
|
|
|
}
|
2023-12-05 17:54:42 +01:00
|
|
|
} else {
|
2024-05-06 10:22:35 +02:00
|
|
|
debug!("Could not find any user {}", stream_req.username);
|
2023-11-22 21:06:58 +01:00
|
|
|
}
|
|
|
|
|
HttpResponse::BadRequest().finish()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2023-12-02 20:47:26 +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>,
|
2023-12-02 20:47:26 +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::LiveAlt, &username, &password, &stream_id, "")).await
|
2023-12-02 20:47:26 +01:00
|
|
|
}
|
|
|
|
|
|
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-06 10:22:35 +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-06 10:22:35 +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-05 19:15:54 +02:00
|
|
|
let action_path = format!("{}/{}/", duration, start);
|
2024-05-06 10:22:35 +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-05-06 18:47:56 +02:00
|
|
|
async fn xtream_get_stream_info(input: &ConfigInput, target: &ConfigTarget, pli: &XtreamPlaylistItem,
|
2024-05-06 10:22:35 +02:00
|
|
|
info_url: &str, cluster: &XtreamCluster) -> Result<String, Error> {
|
|
|
|
|
if let Ok(url) = Url::parse(info_url) {
|
|
|
|
|
let client = request_utils::get_client_request(Some(input), url, None);
|
|
|
|
|
if let Ok(response) = client.send().await {
|
|
|
|
|
debug!("get stream info response status code {}", response.status());
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
match response.text().await {
|
|
|
|
|
Ok(content) => {
|
|
|
|
|
return match cluster {
|
|
|
|
|
XtreamCluster::Live => Ok(content),
|
|
|
|
|
XtreamCluster::Video => {
|
|
|
|
|
if let Ok(mut doc) = serde_json::from_str::<Map<String, Value>>(content.as_str()) {
|
|
|
|
|
if let Some(Value::Object(movie_data) ) = doc.get_mut("movie_data") {
|
2024-05-06 18:47:56 +02:00
|
|
|
let stream_id = pli.id;
|
|
|
|
|
let category_id = pli.category_id;
|
2024-05-06 10:22:35 +02:00
|
|
|
movie_data.insert("stream_id".to_string(), Value::Number(serde_json::value::Number::from(stream_id)));
|
|
|
|
|
movie_data.insert("category_id".to_string(), Value::Number(serde_json::value::Number::from(category_id)));
|
|
|
|
|
let options = XtreamMappingOptions::from_target_options(target.options.as_ref());
|
|
|
|
|
if options.skip_video_direct_source {
|
|
|
|
|
movie_data.insert("direct_source".to_string(), Value::String("".to_string()));
|
|
|
|
|
} else {
|
2024-05-06 18:47:56 +02:00
|
|
|
movie_data.insert("direct_source".to_string(), Value::String(pli.url.to_string()));
|
2024-05-06 10:22:35 +02:00
|
|
|
}
|
|
|
|
|
if let Ok(result) = serde_json::to_string(&doc) {
|
|
|
|
|
return Ok(result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Ok(content);
|
|
|
|
|
}
|
|
|
|
|
XtreamCluster::Series => {
|
|
|
|
|
// With series we have a problem.
|
|
|
|
|
// it could be that the series are resolved and assigned to a new stream_id
|
|
|
|
|
// what now ?
|
|
|
|
|
|
|
|
|
|
return Ok(content);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
Err(err) => { error!("Failed to download info {}", err.to_string()); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(Error::new(std::io::ErrorKind::Other, format!("Cant find stream with id: {}/{}/{}",
|
2024-05-06 18:47:56 +02:00
|
|
|
target.name.as_str(), &cluster, pli.stream_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,
|
2023-12-04 16:19:24 +01:00
|
|
|
cluster: &XtreamCluster) -> HttpResponse {
|
2024-05-06 10:22:35 +02:00
|
|
|
let req_stream_id: u32 = match FromStr::from_str(stream_id) {
|
2024-03-19 10:20:06 +01:00
|
|
|
Ok(id) => id,
|
|
|
|
|
Err(_) => return HttpResponse::BadRequest().finish()
|
|
|
|
|
};
|
2023-12-04 16:19:24 +01:00
|
|
|
|
2024-05-06 18:55:28 +02:00
|
|
|
if let Ok(pli) = get_xtream_item_for_stream_id(req_stream_id, &app_state.config, target, Some(cluster)) {
|
2024-05-06 18:47:56 +02:00
|
|
|
let input_id = pli.input_id;
|
2024-05-06 10:22:35 +02:00
|
|
|
if let Some(input) = app_state.config.get_input_by_id(&input_id) {
|
2024-05-06 18:47:56 +02:00
|
|
|
let stream_id = pli.id;
|
2024-05-06 10:22:35 +02:00
|
|
|
if let Some(info_url) = get_xtream_player_api_info_url(input, cluster, stream_id) {
|
|
|
|
|
if user.proxy == ProxyType::Redirect {
|
2024-04-01 11:59:54 +02:00
|
|
|
return HttpResponse::Found().insert_header(("Location", info_url)).finish();
|
2024-05-06 10:22:35 +02:00
|
|
|
} else if let Ok(content) = xtream_get_stream_info(input, target, &pli, info_url.as_str(), cluster).await {
|
|
|
|
|
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content);
|
2024-04-01 11:59:54 +02:00
|
|
|
}
|
2023-12-04 16:19:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-06 10:22:35 +02:00
|
|
|
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{info:[]}");
|
2023-12-04 16:19:24 +01:00
|
|
|
}
|
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) {
|
|
|
|
|
let req_stream_id: u32 = match FromStr::from_str(stream_id.trim()) {
|
|
|
|
|
Ok(id) => id,
|
|
|
|
|
Err(_) => return HttpResponse::BadRequest().finish()
|
|
|
|
|
};
|
2024-03-18 17:30:52 +01:00
|
|
|
|
2024-05-06 18:55:28 +02:00
|
|
|
if let Ok(pli) = get_xtream_item_for_stream_id(req_stream_id, &app_state.config, target, None) {
|
2024-05-06 18:47:56 +02:00
|
|
|
let input_id: u16 = pli.input_id;
|
2024-05-06 10:22:35 +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, "get_short_epg") {
|
2024-05-06 18:47:56 +02:00
|
|
|
let mut info_url = format!("{}&stream_id={}", action_url, pli.id);
|
2024-05-06 10:22:35 +02:00
|
|
|
if !(limit.is_empty() || limit.eq("0")) {
|
|
|
|
|
info_url = format!("{}&limit={}", info_url, limit);
|
|
|
|
|
}
|
|
|
|
|
if let Ok(url) = Url::parse(&info_url) {
|
|
|
|
|
if user.proxy == ProxyType::Redirect {
|
|
|
|
|
return HttpResponse::Found().insert_header(("Location", info_url)).finish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let client = request_utils::get_client_request(Some(input), url, None);
|
|
|
|
|
if let Ok(response) = client.send().await {
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
return match response.text().await {
|
|
|
|
|
Ok(content) => {
|
|
|
|
|
HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content)
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
error!("Failed to download epg {}", err.to_string());
|
|
|
|
|
HttpResponse::NoContent().finish()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-02 20:47:26 +01: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 {
|
|
|
|
|
"get_live_categories" => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_LIVE),
|
|
|
|
|
"get_vod_categories" => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_VOD),
|
|
|
|
|
"get_series_categories" => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_SERIES),
|
|
|
|
|
_ => Err(std::io::Error::new(ErrorKind::Other, ""))
|
|
|
|
|
} {
|
|
|
|
|
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([("category_id", category_id)])).await);
|
|
|
|
|
} else {
|
|
|
|
|
return Some(serve_file(&file_path, req, mime::APPLICATION_JSON).await);
|
|
|
|
|
}
|
|
|
|
|
} else if let Some(payload) = content {
|
|
|
|
|
return Some(HttpResponse::Ok().body(payload));
|
|
|
|
|
} else {
|
|
|
|
|
return Some(HttpResponse::NoContent().finish());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-02 20:03:22 +01:00
|
|
|
async fn xtream_player_api(
|
2023-12-02 20:47:26 +01:00
|
|
|
req: &HttpRequest,
|
|
|
|
|
api_req: UserApiRequest,
|
|
|
|
|
_app_state: &web::Data<AppState>,
|
2023-10-09 00:17:54 +02:00
|
|
|
) -> HttpResponse {
|
2023-12-02 20:47:26 +01:00
|
|
|
match get_user_target(&api_req, _app_state) {
|
2023-10-25 09:57:44 +02:00
|
|
|
Some((user, target)) => {
|
2023-10-25 21:07:38 +02:00
|
|
|
let action = api_req.action.trim();
|
2023-10-20 12:18:44 +02:00
|
|
|
let target_name = &target.name;
|
2023-10-20 13:32:33 +02:00
|
|
|
if target.has_output(&TargetType::Xtream) {
|
2023-10-25 21:07:38 +02:00
|
|
|
if action.is_empty() {
|
2023-10-26 16:23:32 +02:00
|
|
|
return HttpResponse::Ok().json(get_user_info(&user, &_app_state.config));
|
2023-10-20 13:32:33 +02:00
|
|
|
}
|
2023-11-22 21:06:58 +01:00
|
|
|
|
|
|
|
|
match action {
|
|
|
|
|
"get_series_info" => {
|
2024-04-01 11:59:54 +02:00
|
|
|
xtream_get_stream_info_response(_app_state, &user, target,
|
2023-11-24 22:53:33 +01:00
|
|
|
api_req.series_id.trim(),
|
2023-12-01 21:38:23 +01:00
|
|
|
&XtreamCluster::Series).await
|
2023-11-22 21:06:58 +01:00
|
|
|
}
|
|
|
|
|
"get_vod_info" => {
|
2024-04-01 11:59:54 +02:00
|
|
|
xtream_get_stream_info_response(_app_state, &user, target,
|
2023-11-24 22:53:33 +01:00
|
|
|
api_req.vod_id.trim(),
|
2023-12-01 21:38:23 +01:00
|
|
|
&XtreamCluster::Video).await
|
2023-11-24 22:53:33 +01:00
|
|
|
}
|
2024-03-12 10:25:50 +01:00
|
|
|
"get_epg" |
|
2023-11-24 22:53:33 +01:00
|
|
|
"get_short_epg" => {
|
2024-05-06 10:22:35 +02:00
|
|
|
xtream_get_short_epg(_app_state, &user, target,
|
2023-12-04 16:19:24 +01:00
|
|
|
api_req.stream_id.trim(),
|
|
|
|
|
api_req.limit.trim()).await
|
2023-10-20 13:32:33 +02:00
|
|
|
}
|
2023-11-22 21:06:58 +01:00
|
|
|
_ => {
|
2024-05-06 18:47:56 +02:00
|
|
|
let category_id = api_req.category_id.as_str().trim();
|
|
|
|
|
match xtream_player_api_handle_content_action(&_app_state.config, target_name, action, category_id, req).await {
|
2024-05-06 10:22:35 +02:00
|
|
|
Some(response) => response,
|
|
|
|
|
_ => {
|
2024-05-06 18:47:56 +02:00
|
|
|
let cat_id = if category_id.is_empty() { 0 } else { category_id.parse::<u32>().unwrap_or(0) };
|
2024-05-06 10:22:35 +02:00
|
|
|
match match action {
|
2024-05-06 18:47:56 +02:00
|
|
|
"get_live_streams" => xtream_repository::load_rewrite_xtream_playlist(&XtreamCluster::Live, &_app_state.config, target, cat_id),
|
|
|
|
|
"get_vod_streams" => xtream_repository::load_rewrite_xtream_playlist(&XtreamCluster::Video, &_app_state.config, target, cat_id),
|
|
|
|
|
"get_series" => xtream_repository::load_rewrite_xtream_playlist(&XtreamCluster::Series, &_app_state.config, target, cat_id),
|
2024-05-06 10:22:35 +02:00
|
|
|
_ => Err(Error::new(ErrorKind::Unsupported, format!("Cant find action: {}/{}", target_name, action))),
|
|
|
|
|
} {
|
|
|
|
|
Ok(payload) => HttpResponse::Ok().body(payload),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
debug!("Could not create response for xtream target action: {} {} {}", target_name, action, err);
|
|
|
|
|
HttpResponse::NoContent().finish()
|
2024-03-26 10:26:12 +01:00
|
|
|
}
|
2023-11-22 21:06:58 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-25 21:07:38 +02:00
|
|
|
}
|
2023-10-09 00:17:54 +02:00
|
|
|
}
|
2023-10-20 13:32:33 +02:00
|
|
|
} else {
|
2023-11-28 15:51:08 +01:00
|
|
|
HttpResponse::Ok().json(get_user_info(&user, &_app_state.config))
|
2023-10-09 00:17:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
if api_req.action.is_empty() {
|
2023-10-25 21:07:38 +02:00
|
|
|
debug!("Paremeter action is empty!");
|
2023-10-09 00:17:54 +02:00
|
|
|
HttpResponse::Unauthorized().finish()
|
|
|
|
|
} else {
|
2023-10-25 21:07:38 +02:00
|
|
|
debug!("cant find user!");
|
2023-10-09 00:17:54 +02:00
|
|
|
HttpResponse::BadRequest().finish()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-20 12:18:44 +02:00
|
|
|
}
|
2023-11-02 20:03:22 +01:00
|
|
|
|
2023-12-02 20:47:26 +01:00
|
|
|
|
|
|
|
|
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>,
|
2023-12-02 20:47:26 +01:00
|
|
|
) -> HttpResponse {
|
2024-05-05 19:15:54 +02:00
|
|
|
xtream_player_api(&req, api_req.into_inner(), &app_state).await
|
2023-12-02 20:47:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>,
|
2023-12-02 20:47:26 +01:00
|
|
|
) -> HttpResponse {
|
2024-05-05 19:15:54 +02:00
|
|
|
xtream_player_api(&req, api_req.into_inner(), &app_state).await
|
2023-12-02 20:47:26 +01:00
|
|
|
}
|
|
|
|
|
|
2024-05-02 15:22:23 +02:00
|
|
|
pub(crate) fn xtream_api_register(cfg: &mut web::ServiceConfig) {
|
2024-05-04 20:30:20 +02:00
|
|
|
cfg.service(web::resource("/player_api.php").route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_get)))
|
2024-05-06 10:22:35 +02:00
|
|
|
.service(web::resource("/panel_api.php").route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_get)))
|
|
|
|
|
.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)))
|
|
|
|
|
.service(web::resource("/timeshift/{username}/{password}/{duration}/{start}{stream_id}").route(web::get().to(xtream_player_api_timeshift_stream)));
|
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
|
|
|
}
|