mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-14 20:02:18 +02:00
refactored unwrap calls to make it more stable
This commit is contained in:
@@ -37,6 +37,8 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use url::Url;
|
||||
use crate::api::model::active_user_manager::UserSession;
|
||||
use crate::api::model::provider_config::ProviderConfig;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! try_option_bad_request {
|
||||
@@ -76,10 +78,19 @@ macro_rules! try_result_bad_request {
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! try_unwrap_body {
|
||||
($body:expr) => {
|
||||
$body.map_or_else(
|
||||
|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
|resp| resp.into_response(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub use try_option_bad_request;
|
||||
pub use try_result_bad_request;
|
||||
use crate::api::model::active_user_manager::UserSession;
|
||||
use crate::api::model::provider_config::ProviderConfig;
|
||||
pub use try_unwrap_body;
|
||||
|
||||
pub fn get_server_time() -> String {
|
||||
chrono::offset::Local::now().with_timezone(&chrono::Local).format("%Y-%m-%d %H:%M:%S %Z").to_string()
|
||||
@@ -103,13 +114,11 @@ pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl IntoRes
|
||||
let stream = tokio_util::io::ReaderStream::new(reader);
|
||||
let body = axum::body::Body::from_stream(stream);
|
||||
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime_type.to_string())
|
||||
.header(axum::http::header::CACHE_CONTROL, axum::http::header::HeaderValue::from_static("no-cache"))
|
||||
.body(body)
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.body(body))
|
||||
}
|
||||
Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
};
|
||||
@@ -295,8 +304,6 @@ async fn resolve_streaming_strategy(app_state: &AppState, stream_url: &str, addr
|
||||
None => app_state.active_provider.acquire_connection(&input.name, addr).await
|
||||
};
|
||||
|
||||
error!("{:?}", app_state.active_provider.active_connections().await);
|
||||
|
||||
let stream_response_params = match &**provider_connection_guard {
|
||||
ProviderAllocation::Exhausted => {
|
||||
debug!("Input {} is exhausted. No connections allowed.", input.name);
|
||||
@@ -556,14 +563,14 @@ pub async fn force_provider_stream_response(addr: &str,
|
||||
|
||||
let body_stream = prepare_body_stream(app_state, item_type, stream);
|
||||
debug_if_enabled!("Streaming provider forced stream request from {}", sanitize_sensitive_info(&user_session.stream_url));
|
||||
return response.body(body_stream).unwrap().into_response();
|
||||
return try_unwrap_body!(response.body(body_stream));
|
||||
}
|
||||
drop(stream_details.provider_connection_guard.take());
|
||||
if let (Some(stream), _stream_info) =
|
||||
create_channel_unavailable_stream(&app_state.app_config, &[], axum::http::StatusCode::BAD_GATEWAY)
|
||||
{
|
||||
debug!("Streaming custom stream");
|
||||
axum::response::Response::builder().status(axum::http::StatusCode::OK).body(axum::body::Body::from_stream(stream)).unwrap().into_response()
|
||||
try_unwrap_body!(axum::response::Response::builder().status(axum::http::StatusCode::OK).body(axum::body::Body::from_stream(stream)))
|
||||
} else {
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
@@ -611,7 +618,7 @@ pub async fn stream_response(addr: &str,
|
||||
for (key, value) in &header_map {
|
||||
response = response.header(key, value);
|
||||
}
|
||||
response.body(axum::body::Body::from_stream(broadcast_stream)).unwrap().into_response()
|
||||
try_unwrap_body!(response.body(axum::body::Body::from_stream(broadcast_stream)))
|
||||
} else {
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
@@ -637,7 +644,7 @@ pub async fn stream_response(addr: &str,
|
||||
}
|
||||
|
||||
let body_stream = prepare_body_stream(app_state, item_type, stream);
|
||||
response.body(body_stream).unwrap().into_response()
|
||||
try_unwrap_body!(response.body(body_stream))
|
||||
};
|
||||
|
||||
return stream_resp.into_response();
|
||||
@@ -666,7 +673,7 @@ fn shared_stream_response(app_state: &AppState, stream_url: &str, addr: &str, us
|
||||
for (key, value) in &header_map {
|
||||
response = response.header(key, value);
|
||||
}
|
||||
return Some(response.body(axum::body::Body::from_stream(stream)).unwrap());
|
||||
return response.body(axum::body::Body::from_stream(stream)).ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -743,10 +750,10 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req_hea
|
||||
let writer = BufWriter::new(file);
|
||||
let add_cache_content = get_add_cache_content(resource_url, &app_state.cache);
|
||||
let stream = PersistPipeStream::new(byte_stream, writer, add_cache_content);
|
||||
return response_builder.body(axum::body::Body::from_stream(stream)).unwrap().into_response();
|
||||
return try_unwrap_body!(response_builder.body(axum::body::Body::from_stream(stream)));
|
||||
}
|
||||
}
|
||||
return response_builder.body(axum::body::Body::from_stream(byte_stream)).unwrap().into_response();
|
||||
return try_unwrap_body!(response_builder.body(axum::body::Body::from_stream(byte_stream)));
|
||||
}
|
||||
debug_if_enabled!("Failed to open resource got status {} for {}", status, sanitize_sensitive_info(resource_url));
|
||||
}
|
||||
@@ -770,12 +777,10 @@ pub fn separate_number_and_remainder(input: &str) -> (String, Option<String>) {
|
||||
|
||||
/// # Panics
|
||||
pub fn empty_json_list_response() -> impl IntoResponse + Send {
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header("Content-Type", mime::APPLICATION_JSON.to_string())
|
||||
.body("[]".to_string())
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.body("[]".to_string()))
|
||||
}
|
||||
|
||||
pub fn get_username_from_auth_header(
|
||||
@@ -797,11 +802,10 @@ pub fn get_username_from_auth_header(
|
||||
|
||||
/// # Panics
|
||||
pub fn redirect(url: &str) -> impl IntoResponse {
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::FOUND)
|
||||
.header("Location", url)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap()
|
||||
.body(axum::body::Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn is_seek_request(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::model::{AppConfig, Config, ConfigInput, ConfigTarget};
|
||||
use shared::model::{InputType, M3uPlaylistItem, PlaylistGroup, PlaylistItemType, TargetType, XtreamCluster};
|
||||
use crate::repository::{m3u_repository, xtream_repository};
|
||||
use crate::utils::{m3u, xtream};
|
||||
use crate::utils;
|
||||
use axum::response::IntoResponse;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use shared::model::{InputType, M3uPlaylistItem, PlaylistGroup, PlaylistItemType, TargetType, XtreamCluster};
|
||||
use std::sync::Arc;
|
||||
use indexmap::IndexMap;
|
||||
use crate::utils;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct PlaylistResponseGroup {
|
||||
@@ -57,35 +57,36 @@ where
|
||||
fn group_playlist_items_by_cluster(params: Option<(utils::FileReadGuard,
|
||||
impl Iterator<Item=(M3uPlaylistItem, bool)>)>) ->
|
||||
(Vec<M3uPlaylistItem>, Vec<M3uPlaylistItem>, Vec<M3uPlaylistItem>) {
|
||||
if params.is_none() {
|
||||
return (vec![], vec![], vec![]);
|
||||
}
|
||||
let mut live = Vec::new();
|
||||
let mut video = Vec::new();
|
||||
let mut series = Vec::new();
|
||||
let (guard, iter) = params.unwrap();
|
||||
for (item, _) in iter {
|
||||
match item.item_type {
|
||||
PlaylistItemType::Live
|
||||
| PlaylistItemType::LiveUnknown
|
||||
| PlaylistItemType::LiveHls
|
||||
| PlaylistItemType::LiveDash => {
|
||||
live.push(item);
|
||||
}
|
||||
PlaylistItemType::Catchup
|
||||
| PlaylistItemType::Video => {
|
||||
video.push(item);
|
||||
}
|
||||
PlaylistItemType::Series
|
||||
| PlaylistItemType::SeriesInfo => {
|
||||
series.push(item);
|
||||
match params {
|
||||
None => (vec![], vec![], vec![]),
|
||||
Some((guard, iter)) => {
|
||||
let mut live = Vec::new();
|
||||
let mut video = Vec::new();
|
||||
let mut series = Vec::new();
|
||||
for (item, _) in iter {
|
||||
match item.item_type {
|
||||
PlaylistItemType::Live
|
||||
| PlaylistItemType::LiveUnknown
|
||||
| PlaylistItemType::LiveHls
|
||||
| PlaylistItemType::LiveDash => {
|
||||
live.push(item);
|
||||
}
|
||||
PlaylistItemType::Catchup
|
||||
| PlaylistItemType::Video => {
|
||||
video.push(item);
|
||||
}
|
||||
PlaylistItemType::Series
|
||||
| PlaylistItemType::SeriesInfo => {
|
||||
series.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(guard);
|
||||
|
||||
(live, video, series)
|
||||
}
|
||||
}
|
||||
|
||||
drop(guard);
|
||||
|
||||
(live, video, series)
|
||||
}
|
||||
|
||||
fn group_playlist_groups_by_cluster(playlist: Vec<PlaylistGroup>, input_type: InputType) -> (Vec<PlaylistResponseGroup>, Vec<PlaylistResponseGroup>, Vec<PlaylistResponseGroup>) {
|
||||
@@ -93,7 +94,15 @@ fn group_playlist_groups_by_cluster(playlist: Vec<PlaylistGroup>, input_type: In
|
||||
let mut video = Vec::new();
|
||||
let mut series = Vec::new();
|
||||
for group in playlist {
|
||||
let channels = group.channels.iter().map(|item| if input_type == InputType::M3u { serde_json::to_value(item.to_m3u()).unwrap() } else { serde_json::to_value(item.to_xtream()).unwrap() }).collect();
|
||||
let channels = group.channels.iter()
|
||||
.filter_map(|item| {
|
||||
if input_type == InputType::M3u {
|
||||
serde_json::to_value(item.to_m3u())
|
||||
} else {
|
||||
serde_json::to_value(item.to_xtream())
|
||||
}.ok()
|
||||
})
|
||||
.collect::<serde_json::Value>();
|
||||
let grp = PlaylistResponseGroup {
|
||||
id: group.id,
|
||||
title: group.title,
|
||||
|
||||
@@ -19,49 +19,56 @@ use shared::error::to_io_error;
|
||||
use crate::utils::request::create_client;
|
||||
|
||||
async fn download_file(active: Arc<RwLock<Option<FileDownload>>>, client: &reqwest::Client) -> Result<(), String> {
|
||||
let file_download = { active.read().await.as_ref().unwrap().clone() };
|
||||
match client.get(file_download.url.clone()).send().await {
|
||||
Ok(response) => {
|
||||
match fs::create_dir_all(&file_download.file_dir) {
|
||||
Ok(()) => {
|
||||
if let Some(file_path_str) = file_download.file_path.to_str() {
|
||||
info!("Downloading {file_path_str}");
|
||||
match File::create(&file_download.file_path) {
|
||||
Ok(mut file) => {
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut stream = response.bytes_stream().map_err(to_io_error);
|
||||
loop {
|
||||
match stream.try_next().await {
|
||||
Ok(item) => {
|
||||
if let Some(chunk) = item {
|
||||
match file.write_all(&chunk) {
|
||||
Ok(()) => {
|
||||
downloaded += chunk.len() as u64;
|
||||
active.write().await.as_mut().unwrap().size = downloaded;
|
||||
if let Some(file_download) = active.read().await.as_ref().as_ref() {
|
||||
match client.get(file_download.url.clone()).send().await {
|
||||
Ok(response) => {
|
||||
match fs::create_dir_all(&file_download.file_dir) {
|
||||
Ok(()) => {
|
||||
if let Some(file_path_str) = file_download.file_path.to_str() {
|
||||
info!("Downloading {file_path_str}");
|
||||
match File::create(&file_download.file_path) {
|
||||
Ok(mut file) => {
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut stream = response.bytes_stream().map_err(to_io_error);
|
||||
loop {
|
||||
match stream.try_next().await {
|
||||
Ok(item) => {
|
||||
if let Some(chunk) = item {
|
||||
match file.write_all(&chunk) {
|
||||
Ok(()) => {
|
||||
downloaded += chunk.len() as u64;
|
||||
if let Some(lock) = active.write().await.as_mut() {
|
||||
lock.size = downloaded;
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {file_path_str} {err}"))
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {file_path_str} {err}"))
|
||||
} else {
|
||||
let megabytes = bytes_to_megabytes(downloaded);
|
||||
info!("Downloaded {file_path_str}, filesize: {megabytes}MB");
|
||||
if let Some(lock) = active.write().await.as_mut() {
|
||||
lock.size = downloaded;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
let megabytes = bytes_to_megabytes(downloaded);
|
||||
info!("Downloaded {file_path_str}, filesize: {megabytes}MB");
|
||||
active.write().await.as_mut().unwrap().size = downloaded;
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {file_path_str} {err}"))
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {file_path_str} {err}"))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("Error while writing to file: {file_path_str} {err}"))
|
||||
}
|
||||
Err(err) => Err(format!("Error while writing to file: {file_path_str} {err}"))
|
||||
} else {
|
||||
Err("Error file-download file-path unknown".to_string())
|
||||
}
|
||||
} else {
|
||||
Err("Error file-download file-path unknown".to_string())
|
||||
}
|
||||
Err(err) => Err(format!("Error while creating directory for file: {} {}", &file_download.file_dir.to_str().unwrap_or("?"), err))
|
||||
}
|
||||
Err(err) => Err(format!("Error while creating directory for file: {} {}", &file_download.file_dir.to_str().unwrap_or("?"), err))
|
||||
}
|
||||
Err(err) => Err(format!("Error while opening url: {} {}", &file_download.url, err))
|
||||
}
|
||||
Err(err) => Err(format!("Error while opening url: {} {}", &file_download.url, err))
|
||||
} else {
|
||||
Err("No active file download".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,25 +128,29 @@ pub async fn queue_download_file(
|
||||
let app_config = &*app_state.app_config;
|
||||
|
||||
let config = <Arc<ArcSwap<Config>> as Access<Config>>::load(&app_config.config);
|
||||
if let Some(download_cfg) = config.video.as_ref().unwrap().download.as_ref() {
|
||||
if download_cfg.directory.is_none() {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download.directory configuration"}))).into_response();
|
||||
}
|
||||
match FileDownload::new(req.url.as_str(), req.filename.as_str(), download_cfg) {
|
||||
Some(file_download) => {
|
||||
app_state.downloads.queue.lock().await.push_back(file_download.clone());
|
||||
if app_state.downloads.active.read().await.is_none() {
|
||||
match run_download_queue(&app_state.app_config, download_cfg, &app_state.downloads).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err}))).into_response(),
|
||||
}
|
||||
}
|
||||
axum::Json(download_info!(&file_download)).into_response()
|
||||
if let Some(video_cfg) = config.video.as_ref() {
|
||||
if let Some(download_cfg) = video_cfg.download.as_ref() {
|
||||
if download_cfg.directory.is_empty() {
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download.directory configuration"}))).into_response();
|
||||
}
|
||||
None => (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(),
|
||||
match FileDownload::new(req.url.as_str(), req.filename.as_str(), download_cfg) {
|
||||
Some(file_download) => {
|
||||
app_state.downloads.queue.lock().await.push_back(file_download.clone());
|
||||
if app_state.downloads.active.read().await.is_none() {
|
||||
match run_download_queue(&app_state.app_config, download_cfg, &app_state.downloads).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err}))).into_response(),
|
||||
}
|
||||
}
|
||||
axum::Json(download_info!(&file_download)).into_response()
|
||||
}
|
||||
None => (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(),
|
||||
}
|
||||
} else {
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download configuration"}))).into_response()
|
||||
}
|
||||
} else {
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download configuration"}))).into_response()
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video configuration"}))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use shared::utils::get_string_from_serde_value;
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
// https://info.hdhomerun.com/info/http_api
|
||||
|
||||
// const DISCOVERY_BYTES: &[u8] = &[0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143];
|
||||
@@ -184,12 +185,10 @@ fn create_device(app_state: &Arc<HdHomerunAppState>) -> Option<Device> {
|
||||
|
||||
async fn device_xml(axum::extract::State(app_state): axum::extract::State<Arc<HdHomerunAppState>>) -> impl IntoResponse {
|
||||
if let Some(device) = create_device(&app_state) {
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/xml")
|
||||
.body(axum::body::Body::from(device.as_xml()))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.body(axum::body::Body::from(device.as_xml())))
|
||||
} else {
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
@@ -231,11 +230,10 @@ async fn lineup(app_state: &Arc<HdHomerunAppState>, cfg: &Arc<AppConfig>, creden
|
||||
let body_stream = stream::once(async { Ok(Bytes::from("[")) })
|
||||
.chain(stream)
|
||||
.chain(stream::once(async { Ok(Bytes::from("]")) }));
|
||||
return axum::response::Response::builder()
|
||||
return try_unwrap_body!(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(body_stream))
|
||||
.unwrap().into_response();
|
||||
.body(axum::body::Body::from_stream(body_stream)));
|
||||
} else if (use_all || use_xtream) && target.has_output(&TargetType::Xtream) {
|
||||
let server_info = app_state.app_state.app_config.get_user_server_info(credentials);
|
||||
let base_url = server_info.get_base_url();
|
||||
@@ -262,12 +260,10 @@ async fn lineup(app_state: &Arc<HdHomerunAppState>, cfg: &Arc<AppConfig>, creden
|
||||
.chain(comma_stream)
|
||||
.chain(vod_stream_peek)
|
||||
.chain(stream::once(async { Ok(Bytes::from("]")) }));
|
||||
return axum::response::Response::builder()
|
||||
return try_unwrap_body!(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(body_stream))
|
||||
.unwrap()
|
||||
.into_response();
|
||||
.body(axum::body::Body::from_stream(body_stream)));
|
||||
}
|
||||
axum::http::StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use crate::api::api_utils::{force_provider_stream_response, get_stream_alternative_url, is_seek_request};
|
||||
use crate::api::api_utils::{try_option_bad_request};
|
||||
use crate::api::api_utils::try_option_bad_request;
|
||||
use crate::api::model::active_user_manager::UserSession;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::streams::provider_stream::{create_custom_video_stream_response, CustomVideoStreamType};
|
||||
use crate::model::{ProxyUserCredentials};
|
||||
use crate::auth::Fingerprint;
|
||||
use crate::model::ConfigInput;
|
||||
use shared::model::{PlaylistItemType, UserConnectionPermission, XtreamCluster};
|
||||
use crate::model::ProxyUserCredentials;
|
||||
use crate::processing::parser::hls::{get_hls_session_token_and_url_from_token, rewrite_hls, RewriteHlsProps};
|
||||
use shared::utils::{is_hls_url, replace_url_extension, sanitize_sensitive_info, HLS_EXT};
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
use crate::utils::request;
|
||||
use axum::response::IntoResponse;
|
||||
use log::{debug, error};
|
||||
use serde::Deserialize;
|
||||
use shared::model::{PlaylistItemType, UserConnectionPermission, XtreamCluster};
|
||||
use shared::utils::{is_hls_url, replace_url_extension, sanitize_sensitive_info, HLS_EXT};
|
||||
use std::sync::Arc;
|
||||
use crate::api::model::active_user_manager::UserSession;
|
||||
use crate::auth::Fingerprint;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HlsApiPathParams {
|
||||
@@ -25,24 +26,22 @@ struct HlsApiPathParams {
|
||||
}
|
||||
|
||||
fn hls_response(hls_content: String) -> impl IntoResponse + Send {
|
||||
let builder = axum::response::Response::builder()
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/x-mpegurl");
|
||||
builder.body(hls_content)
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/x-mpegurl")
|
||||
.body(hls_content))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(in crate::api) async fn handle_hls_stream_request(
|
||||
fingerprint: &str, addr: &str,
|
||||
app_state: &Arc<AppState>,
|
||||
user: &ProxyUserCredentials,
|
||||
user_session: Option<&UserSession>,
|
||||
hls_url: &str,
|
||||
virtual_id: u32,
|
||||
input: &ConfigInput,
|
||||
connection_permission: UserConnectionPermission) -> impl IntoResponse + Send {
|
||||
fingerprint: &str, addr: &str,
|
||||
app_state: &Arc<AppState>,
|
||||
user: &ProxyUserCredentials,
|
||||
user_session: Option<&UserSession>,
|
||||
hls_url: &str,
|
||||
virtual_id: u32,
|
||||
input: &ConfigInput,
|
||||
connection_permission: UserConnectionPermission) -> impl IntoResponse + Send {
|
||||
let url = replace_url_extension(hls_url, HLS_EXT);
|
||||
let server_info = app_state.app_config.get_user_server_info(user);
|
||||
|
||||
@@ -52,18 +51,18 @@ pub(in crate::api) async fn handle_hls_stream_request(
|
||||
Some(provider_cfg) => {
|
||||
let stream_url = get_stream_alternative_url(&url, input, &provider_cfg);
|
||||
(stream_url, Some(session.token.to_string()))
|
||||
},
|
||||
}
|
||||
None => (url, None),
|
||||
}
|
||||
},
|
||||
}
|
||||
None => {
|
||||
match app_state.active_provider.get_next_provider(&input.name).await {
|
||||
Some(provider_cfg) => {
|
||||
let stream_url = get_stream_alternative_url(&url, input, &provider_cfg);
|
||||
let user_session_token = format!("{fingerprint}{virtual_id}");
|
||||
let session_token= app_state.active_users.create_user_session(user, &user_session_token, virtual_id, &provider_cfg.name, &stream_url, addr, connection_permission);
|
||||
let session_token = app_state.active_users.create_user_session(user, &user_session_token, virtual_id, &provider_cfg.name, &stream_url, addr, connection_permission);
|
||||
(stream_url, Some(session_token))
|
||||
},
|
||||
}
|
||||
None => (url, None),
|
||||
}
|
||||
}
|
||||
@@ -110,7 +109,7 @@ async fn hls_api_stream(
|
||||
let user_session_token = format!("{fingerprint}{virtual_id}");
|
||||
let mut user_session = app_state.active_users.get_user_session(&user.username, &user_session_token);
|
||||
|
||||
if let Some(session) = &mut user_session {
|
||||
if let Some(session) = &mut user_session {
|
||||
if session.permission == UserConnectionPermission::Exhausted {
|
||||
return create_custom_video_stream_response(&app_state.app_config, CustomVideoStreamType::UserConnectionsExhausted).into_response();
|
||||
}
|
||||
@@ -128,7 +127,7 @@ async fn hls_api_stream(
|
||||
if session.virtual_id == virtual_id {
|
||||
if is_seek_request(XtreamCluster::Live, &req_headers).await {
|
||||
// partial request means we are in reverse proxy mode, seek happened
|
||||
return force_provider_stream_response(&addr, &app_state, session, PlaylistItemType::LiveHls, &req_headers, &input, &user).await.into_response()
|
||||
return force_provider_stream_response(&addr, &app_state, session, PlaylistItemType::LiveHls, &req_headers, &input, &user).await.into_response();
|
||||
}
|
||||
} else {
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
|
||||
@@ -14,6 +14,7 @@ use futures::stream;
|
||||
use log::{debug, error};
|
||||
use std::sync::Arc;
|
||||
use crate::auth::Fingerprint;
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
|
||||
async fn m3u_api(
|
||||
api_req: &UserApiRequest,
|
||||
@@ -32,7 +33,7 @@ async fn m3u_api(
|
||||
if api_req.content_type == "m3u_plus" {
|
||||
builder = builder.header("Content-Disposition", "attachment; filename=\"playlist.m3u\"");
|
||||
}
|
||||
builder.body(axum::body::Body::from_stream(content_stream)).unwrap().into_response()
|
||||
try_unwrap_body!(builder.body(axum::body::Body::from_stream(content_stream)))
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{}", sanitize_sensitive_info(err.to_string().as_str()));
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use axum::response::IntoResponse;
|
||||
use crate::auth::AuthBearer;
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
|
||||
fn get_categories_from_xtream(categories: Option<Vec<PlaylistXtreamCategory>>) -> Vec<String> {
|
||||
let mut groups: Vec<String> = Vec::new();
|
||||
@@ -86,13 +87,10 @@ async fn playlist_categories(
|
||||
.chain(m3u_stream)
|
||||
.chain(stream::once(async { Ok::<Bytes, String>(Bytes::from("}")) }));
|
||||
|
||||
|
||||
return axum::response::Response::builder()
|
||||
return try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header("Content-Type", mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from_stream(json_stream))
|
||||
.unwrap()
|
||||
.into_response();
|
||||
.body(axum::body::Body::from_stream(json_stream)));
|
||||
}
|
||||
}
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
@@ -134,20 +132,18 @@ async fn playlist_bouquet(
|
||||
let config = &app_state.app_config.config.load();
|
||||
let xtream = load_user_bouquet_as_json(config, &username, TargetType::Xtream).await;
|
||||
let m3u = load_user_bouquet_as_json(config, &username, TargetType::M3u).await;
|
||||
return axum::response::Response::builder()
|
||||
return try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header("Content-Type", mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from(format!(r#"{{"xtream": {}, "m3u": {} }}"#, xtream.unwrap_or("null".to_string()), m3u.unwrap_or("null".to_string()))))
|
||||
.unwrap()
|
||||
.into_response();
|
||||
.body(axum::body::Body::from(format!(r#"{{"xtream": {}, "m3u": {} }}"#,
|
||||
xtream.unwrap_or("null".to_string()),
|
||||
m3u.unwrap_or("null".to_string())))));
|
||||
}
|
||||
}
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header("Content-Type", mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from("{}"))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.body(axum::body::Body::from("{}")))
|
||||
}
|
||||
|
||||
pub fn user_api_register(app_state: Arc<AppState>) -> axum::Router<Arc<AppState>> {
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::processing::processor::playlist;
|
||||
use crate::repository::user_repository::store_api_user;
|
||||
use crate::utils::ip_checker::get_ips;
|
||||
use crate::{utils, VERSION};
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
use axum::response::IntoResponse;
|
||||
use log::error;
|
||||
use serde_json::json;
|
||||
@@ -79,9 +80,9 @@ async fn save_config_api_proxy_user(
|
||||
}
|
||||
} else {
|
||||
let config = app_state.app_config.config.load();
|
||||
let backup_dir = config.backup_dir.as_ref().unwrap().as_str();
|
||||
let backup_dir = config.get_backup_dir();
|
||||
let paths = app_state.app_config.paths.load();
|
||||
if let Some(err) = intern_save_config_api_proxy(backup_dir, &ApiProxyConfigDto::from(&*new_api_proxy), paths.api_proxy_file_path.as_str()) {
|
||||
if let Some(err) = intern_save_config_api_proxy(backup_dir.as_ref(), &ApiProxyConfigDto::from(&*new_api_proxy), paths.api_proxy_file_path.as_str()) {
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
}
|
||||
@@ -97,8 +98,8 @@ async fn save_config_main(
|
||||
let paths = app_state.app_config.paths.load();
|
||||
let file_path = paths.config_file_path.as_str();
|
||||
let config = app_state.app_config.config.load();
|
||||
let backup_dir = config.backup_dir.as_ref().unwrap().as_str();
|
||||
if let Some(err) = intern_save_config_main(file_path, backup_dir, &cfg) {
|
||||
let backup_dir = config.get_backup_dir();
|
||||
if let Some(err) = intern_save_config_main(file_path, backup_dir.as_ref(), &cfg) {
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
axum::http::StatusCode::OK.into_response()
|
||||
@@ -117,16 +118,16 @@ async fn save_config_api_proxy_config(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO wenn hot reload an ist wird doppelt geladen
|
||||
// TODO if hot reload is on, loaded twice
|
||||
if let Some(old_api_proxy) = app_state.app_config.api_proxy.load().clone() {
|
||||
let mut api_proxy = (*old_api_proxy).clone();
|
||||
api_proxy.server = req_api_proxy.iter().map(Into::into).collect();
|
||||
let new_api_proxy = Arc::new(api_proxy);
|
||||
app_state.app_config.api_proxy.store(Some(Arc::clone(&new_api_proxy)));
|
||||
let config = app_state.app_config.config.load();
|
||||
let backup_dir = config.backup_dir.as_ref().unwrap().as_str();
|
||||
let backup_dir = config.get_backup_dir();
|
||||
let paths = app_state.app_config.paths.load();
|
||||
if let Some(err) = intern_save_config_api_proxy(backup_dir, &ApiProxyConfigDto::from(new_api_proxy.as_ref()), paths.api_proxy_file_path.as_str()) {
|
||||
if let Some(err) = intern_save_config_api_proxy(backup_dir.as_ref(), &ApiProxyConfigDto::from(new_api_proxy.as_ref()), paths.api_proxy_file_path.as_str()) {
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
}
|
||||
@@ -318,8 +319,8 @@ pub async fn create_status_check(app_state: &Arc<AppState>) -> StatusCheck {
|
||||
async fn status(axum::extract::State(app_state): axum::extract::State<Arc<AppState>>) -> axum::response::Response {
|
||||
let status = create_status_check(&app_state).await;
|
||||
match serde_json::to_string_pretty(&status) {
|
||||
Ok(pretty_json) => axum::response::Response::builder().status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()).body(pretty_json).unwrap().into_response(),
|
||||
Ok(pretty_json) => try_unwrap_body!(axum::response::Response::builder().status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()).body(pretty_json)),
|
||||
Err(_) => axum::Json(status).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -331,8 +332,8 @@ async fn ipinfo(axum::extract::State(app_state): axum::extract::State<Arc<AppSta
|
||||
ipv6,
|
||||
};
|
||||
return match serde_json::to_string(&ipcheck) {
|
||||
Ok(json) => axum::response::Response::builder().status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()).body(json).unwrap().into_response(),
|
||||
Ok(json) => try_unwrap_body!(axum::response::Response::builder().status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()).body(json)),
|
||||
Err(_) => axum::Json(ipcheck).into_response(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::api::api_utils::serve_file;
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::auth::{AuthBearer, verify_password, create_jwt_admin, create_jwt_user, is_admin, verify_token};
|
||||
use crate::auth::{create_jwt_admin, create_jwt_user, is_admin, verify_password, verify_token, AuthBearer};
|
||||
use axum::response::IntoResponse;
|
||||
use log::error;
|
||||
use serde_json::json;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc};
|
||||
use tower::Service;
|
||||
use shared::model::{TokenResponse, UserCredential};
|
||||
use shared::utils::CONSTANTS;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tower::Service;
|
||||
|
||||
fn no_web_auth_token() -> impl axum::response::IntoResponse + Send {
|
||||
axum::Json(TokenResponse {
|
||||
@@ -114,10 +115,9 @@ async fn index(
|
||||
new_content.replace_range(pos..pos + 6, &base_href);
|
||||
}
|
||||
|
||||
return axum::response::Response::builder()
|
||||
return try_unwrap_body!(axum::response::Response::builder()
|
||||
.header("Content-Type", mime::TEXT_HTML_UTF_8.as_ref())
|
||||
.body(new_content.into())
|
||||
.unwrap();
|
||||
.body(new_content));
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to read web ui index.hml: {err}");
|
||||
@@ -151,10 +151,9 @@ async fn index_config(
|
||||
}
|
||||
}
|
||||
if let Ok(json_content) = serde_json::to_string(&json_data) {
|
||||
return axum::response::Response::builder()
|
||||
return try_unwrap_body!(axum::response::Response::builder()
|
||||
.header("Content-Type", mime::APPLICATION_JSON.as_ref())
|
||||
.body(axum::body::Body::from(json_content))
|
||||
.unwrap();
|
||||
.body(axum::body::Body::from(json_content)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,8 +207,7 @@ pub fn index_register_with_path(web_dir_path: &Path, web_ui_path: &str) -> axum:
|
||||
let new_req = axum::http::Request::builder()
|
||||
.method(req.method())
|
||||
.uri(new_uri)
|
||||
.body(req.into_body())
|
||||
.unwrap();
|
||||
.body(req.into_body()).unwrap();
|
||||
|
||||
serve_dir.call(new_req)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::ws::{WebSocketUpgrade, WebSocket, Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum::extract::ws::CloseFrame;
|
||||
use log::{error, info};
|
||||
use shared::model::{ProtocolHandler, ProtocolMessage, WsCloseCode, PROTOCOL_VERSION};
|
||||
use crate::api::endpoints::v1_api::create_status_check;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::auth::verify_token;
|
||||
use crate::auth::verify_token_admin;
|
||||
use axum::extract::ws::CloseFrame;
|
||||
use axum::{
|
||||
extract::ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use log::{error, info};
|
||||
use shared::model::{ProtocolHandler, ProtocolMessage, WsCloseCode, PROTOCOL_VERSION};
|
||||
use std::sync::Arc;
|
||||
|
||||
// WebSocket upgrade handler
|
||||
async fn websocket_handler(
|
||||
@@ -34,8 +34,17 @@ pub fn ws_api_register(web_auth_enabled: bool, web_ui_path: &str) -> axum::Route
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn verify_auth_admin_token(auth_token: &str, secret_key: Option<&Vec<u8>>) -> bool {
|
||||
match secret_key.as_ref() {
|
||||
None => false,
|
||||
Some(key) => verify_token_admin(auth_token, key.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// WebSocket communication logic
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn handle_socket(mut socket: WebSocket, app_state: Arc<AppState>, auth: bool) {
|
||||
let secret_key = if auth {
|
||||
if let Some(web_auth_config) = &app_state.app_config.config.load().web_ui.as_ref().and_then(|c| c.auth.as_ref()) {
|
||||
@@ -48,13 +57,8 @@ async fn handle_socket(mut socket: WebSocket, app_state: Arc<AppState>, auth: bo
|
||||
None
|
||||
};
|
||||
|
||||
let verify_auth_token = |auth_token: &str| {
|
||||
secret_key.as_ref().map(|key| verify_token(auth_token, key.as_slice()))
|
||||
};
|
||||
|
||||
let mut active_user_change_rx = app_state.active_users.get_active_user_change_channel();
|
||||
let mut active_provider_change_rx = app_state.active_provider.get_active_provider_change_channel();
|
||||
|
||||
let mut active_user_change_rx = app_state.active_users.get_active_user_change_channel();
|
||||
let mut active_provider_change_rx = app_state.active_provider.get_active_provider_change_channel();
|
||||
|
||||
let mut handler = ProtocolHandler::Version(PROTOCOL_VERSION);
|
||||
|
||||
@@ -94,7 +98,7 @@ async fn handle_socket(mut socket: WebSocket, app_state: Arc<AppState>, auth: bo
|
||||
if let Message::Binary(bytes) = msg {
|
||||
match ProtocolMessage::from_bytes(bytes) {
|
||||
Ok(ProtocolMessage::StatusRequest(auth_token)) => {
|
||||
if !auth || verify_auth_token(&auth_token).is_some() {
|
||||
if !auth || verify_auth_admin_token(&auth_token, secret_key.as_ref()) {
|
||||
let status = create_status_check(&app_state).await;
|
||||
if let Ok(response) = ProtocolMessage::StatusResponse(status).to_bytes() {
|
||||
if socket.send(Message::Binary(response)).await.is_err() {
|
||||
|
||||
@@ -12,19 +12,19 @@ use std::sync::Arc;
|
||||
use crate::api::api_utils::{get_user_target, serve_file};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::model::Config;
|
||||
use crate::model::{ConfigTarget, ProxyUserCredentials, TargetOutput};
|
||||
use crate::model::{Config};
|
||||
use crate::repository::m3u_repository::m3u_get_epg_file_path;
|
||||
use crate::repository::storage::get_target_storage_path;
|
||||
use crate::repository::xtream_repository::{xtream_get_epg_file_path, xtream_get_storage_path};
|
||||
use crate::utils;
|
||||
use crate::{utils};
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
|
||||
pub fn get_empty_epg_response() -> impl axum::response::IntoResponse + Send {
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK) // Entspricht `HttpResponse::Ok()`
|
||||
.header(axum::http::header::CONTENT_TYPE, axum::http::HeaderValue::from_static("text/xml"))
|
||||
.body(axum::body::Body::from(r#"<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE tv SYSTEM "xmltv.dtd"><tv generator-info-name="Xtream Codes" generator-info-url=""></tv>"#)) // Setzt den Body der Antwort
|
||||
.unwrap()
|
||||
.body(axum::body::Body::from(r#"<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE tv SYSTEM "xmltv.dtd"><tv generator-info-name="Xtream Codes" generator-info-url=""></tv>"#)))
|
||||
}
|
||||
|
||||
fn time_correct(date_time: &str, correction: &TimeDelta) -> String {
|
||||
@@ -159,14 +159,14 @@ fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> impl axum::r
|
||||
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
let compressed_data = xml_writer.into_inner().finish().unwrap();
|
||||
axum::response::Response::builder()
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM.to_string())
|
||||
.header(axum::http::header::CONTENT_ENCODING, "gzip") // Set Content-Encoding header
|
||||
.body(axum::body::Body::from(compressed_data))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
match xml_writer.into_inner().finish() {
|
||||
Ok(compressed_data) =>
|
||||
try_unwrap_body!(axum::response::Response::builder()
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM.to_string())
|
||||
.header(axum::http::header::CONTENT_ENCODING, "gzip") // Set Content-Encoding header
|
||||
.body(axum::body::Body::from(compressed_data))),
|
||||
Err(err) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles XMLTV EPG API requests, serving the appropriate EPG file with optional time-shifting based on user configuration.
|
||||
|
||||
@@ -38,6 +38,7 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use shared::model::{PlaylistItemType, XtreamCluster, FieldGetAccessor, PlaylistEntry, TargetType, UserConnectionPermission, ProxyType, get_backdrop_path_value, XtreamPlaylistItem};
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum ApiStreamContext {
|
||||
@@ -626,12 +627,10 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC
|
||||
if user.proxy == ProxyType::Redirect && cluster == XtreamCluster::Live {
|
||||
return redirect(&info_url).into_response();
|
||||
} else if let Ok(content) = xtream::get_xtream_stream_info(Arc::clone(&app_state.http_client.load()), &app_state.app_config, user, &input, target, &pli, info_url.as_str(), cluster).await {
|
||||
return axum::response::Response::builder()
|
||||
return try_unwrap_body!(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(content))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.body(axum::body::Body::from(content)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -640,27 +639,21 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC
|
||||
return match cluster {
|
||||
XtreamCluster::Video => {
|
||||
let content = create_vod_info_from_item(target, user, &pli, virtual_record.last_updated);
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(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(content))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.body(axum::body::Body::from(content)))
|
||||
}
|
||||
XtreamCluster::Live | XtreamCluster::Series => axum::response::Response::builder()
|
||||
XtreamCluster::Live | XtreamCluster::Series => try_unwrap_body!(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("{}".as_bytes()))
|
||||
.unwrap()
|
||||
.into_response(),
|
||||
.body(axum::body::Body::from("{}".as_bytes()))),
|
||||
};
|
||||
}
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(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("{}".as_bytes()))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
.body(axum::body::Body::from("{}".as_bytes())))
|
||||
}
|
||||
|
||||
async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, limit: &str) -> impl IntoResponse + Send {
|
||||
@@ -721,9 +714,9 @@ async fn xtream_player_api_handle_content_action(config: &Config, target_name: &
|
||||
}
|
||||
return Some(serve_file(&file_path, mime::APPLICATION_JSON).await.into_response());
|
||||
} else if let Some(payload) = content {
|
||||
return Some(axum::response::Response::builder()
|
||||
return Some(try_unwrap_body!(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.body(payload).unwrap().into_response());
|
||||
.body(payload)));
|
||||
}
|
||||
return Some(api_utils::empty_json_list_response().into_response());
|
||||
}
|
||||
@@ -758,10 +751,10 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget
|
||||
serde_json::to_string(&doc)
|
||||
.map_or_else(
|
||||
|_| axum::http::StatusCode::BAD_REQUEST.into_response(),
|
||||
|result| axum::response::Response::builder()
|
||||
|result| try_unwrap_body!(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())
|
||||
.body(result)))
|
||||
}
|
||||
|
||||
macro_rules! skip_json_response_if_flag_set {
|
||||
@@ -848,8 +841,7 @@ async fn xtream_player_api(
|
||||
skip_flag_optional!(skip_vod, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Video, &app_state.app_config, &target, category_id, &user).await),
|
||||
crate::model::XC_ACTION_GET_SERIES =>
|
||||
skip_flag_optional!(skip_series, xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Series, &app_state.app_config, &target, category_id, &user).await),
|
||||
_ => Some(Err(info_err!(format!("Cant find content: {action} for target: {}", &target.name))
|
||||
)),
|
||||
_ => Some(Err(info_err!(format!("Cant find content: {action} for target: {}", &target.name)))),
|
||||
};
|
||||
|
||||
match result {
|
||||
@@ -858,10 +850,10 @@ async fn xtream_player_api(
|
||||
Ok(xtream_iter) => {
|
||||
// Convert the iterator into a stream of `Bytes`
|
||||
let content_stream = xtream_create_content_stream(xtream_iter);
|
||||
axum::response::Response::builder()
|
||||
try_unwrap_body!(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()
|
||||
.body(axum::body::Body::from_stream(content_stream)))
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed response for xtream target: {} action: {} error: {}", &target.name, action, err);
|
||||
|
||||
+16
-16
@@ -1,8 +1,11 @@
|
||||
use crate::api::api_utils::{get_build_time, get_server_time};
|
||||
use crate::api::config_watch::exec_config_watch;
|
||||
use crate::api::endpoints::hdhomerun_api::hdhr_api_register;
|
||||
use crate::api::endpoints::hls_api::hls_api_register;
|
||||
use crate::api::endpoints::m3u_api::m3u_api_register;
|
||||
use crate::api::endpoints::v1_api::v1_api_register;
|
||||
use crate::api::endpoints::web_index::{index_register_with_path, index_register_without_path};
|
||||
use crate::api::endpoints::websocket_api::ws_api_register;
|
||||
use crate::api::endpoints::xmltv_api::xmltv_api_register;
|
||||
use crate::api::endpoints::xtream_api::xtream_api_register;
|
||||
use crate::api::model::active_provider_manager::ActiveProviderManager;
|
||||
@@ -10,23 +13,20 @@ use crate::api::model::active_user_manager::ActiveUserManager;
|
||||
use crate::api::model::app_state::{create_cache, create_http_client, AppState, CancelTokens, HdHomerunAppState};
|
||||
use crate::api::model::download::DownloadQueue;
|
||||
use crate::api::model::streams::shared_stream_manager::SharedStreamManager;
|
||||
use crate::api::scheduler::{exec_scheduler};
|
||||
use crate::api::scheduler::exec_scheduler;
|
||||
use crate::api::serve::serve;
|
||||
use crate::model::{AppConfig, Config, ProcessTargets, RateLimitConfig};
|
||||
use crate::model::{Healthcheck};
|
||||
use crate::model::Healthcheck;
|
||||
use crate::processing::processor::playlist;
|
||||
use crate::VERSION;
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use axum::Router;
|
||||
use log::{error, info};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use axum::Router;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tower_governor::key_extractor::SmartIpKeyExtractor;
|
||||
use crate::api::api_utils::{get_build_time, get_server_time};
|
||||
use crate::api::config_watch::exec_config_watch;
|
||||
use crate::api::endpoints::websocket_api::ws_api_register;
|
||||
use crate::api::serve::serve;
|
||||
use crate::VERSION;
|
||||
|
||||
fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result<PathBuf, std::io::Error> {
|
||||
let web_dir = web_root.to_string();
|
||||
@@ -232,7 +232,7 @@ pub async fn start_server(app_config: Arc<AppConfig>, targets: Arc<ProcessTarget
|
||||
|
||||
router = router.layer(create_cors_layer())
|
||||
.layer(create_compression_layer());
|
||||
//.layer(tower_http::trace::TraceLayer::new_for_http()); // `Logger::default()`
|
||||
//.layer(tower_http::trace::TraceLayer::new_for_http()); // `Logger::default()`
|
||||
// router = router.layer(axum::middleware::from_fn(log_routes));
|
||||
|
||||
let router: axum::Router<()> = router.with_state(shared_data.clone());
|
||||
@@ -246,15 +246,15 @@ pub async fn start_server(app_config: Arc<AppConfig>, targets: Arc<ProcessTarget
|
||||
|
||||
fn add_rate_limiter(router: Router<Arc<AppState>>, rate_limit_cfg: &RateLimitConfig) -> Router<Arc<AppState>> {
|
||||
if rate_limit_cfg.enabled {
|
||||
let governor_conf = Arc::new(tower_governor::governor::GovernorConfigBuilder::default()
|
||||
let governor_conf = tower_governor::governor::GovernorConfigBuilder::default()
|
||||
.key_extractor(SmartIpKeyExtractor)
|
||||
.per_millisecond(rate_limit_cfg.period_millis)
|
||||
.burst_size(rate_limit_cfg.burst_size)
|
||||
.finish()
|
||||
.unwrap());
|
||||
router.layer(tower_governor::GovernorLayer {
|
||||
config: governor_conf,
|
||||
})
|
||||
.finish();
|
||||
if let Some(config) = governor_conf { router.layer(tower_governor::GovernorLayer { config: Arc::new(config) }) } else {
|
||||
error!("Failed to initialize rate limiter");
|
||||
router
|
||||
}
|
||||
} else {
|
||||
router
|
||||
}
|
||||
|
||||
@@ -335,11 +335,12 @@ impl MultiProviderLineup {
|
||||
let mut idx = index.load(Ordering::Relaxed) % provider_count;
|
||||
let start = idx;
|
||||
for _ in start..provider_count {
|
||||
let p = pg.get(idx).unwrap();
|
||||
let result = p.get_next(grace, grace_period_timeout_secs).await;
|
||||
if result.is_some() {
|
||||
index.store((idx + 1) % provider_count, Ordering::Relaxed);
|
||||
return result;
|
||||
if let Some(p) = pg.get(idx) {
|
||||
let result = p.get_next(grace, grace_period_timeout_secs).await;
|
||||
if result.is_some() {
|
||||
index.store((idx + 1) % provider_count, Ordering::Relaxed);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
idx = (idx + 1) % provider_count;
|
||||
}
|
||||
|
||||
@@ -52,10 +52,10 @@ fn get_download_directory(download_cfg: &VideoDownloadConfig, filestem: &str) ->
|
||||
}
|
||||
}
|
||||
let dir_name = CONSTANTS.re_remove_filename_ending.replace(stem, "");
|
||||
let file_dir: PathBuf = [download_cfg.directory.as_ref().unwrap(), dir_name.as_ref()].iter().collect();
|
||||
let file_dir: PathBuf = [download_cfg.directory.as_str(), dir_name.as_ref()].iter().collect();
|
||||
file_dir
|
||||
} else {
|
||||
PathBuf::from(download_cfg.directory.as_ref().unwrap())
|
||||
PathBuf::from(download_cfg.directory.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ use shared::utils::{filter_response_header};
|
||||
pub fn get_response_headers(headers: &HeaderMap) -> Vec<(String, String)> {
|
||||
let mut response_headers: Vec<(String, String)> = headers.iter()
|
||||
.filter(|(key, _)| filter_response_header(key.as_str()))
|
||||
.map(|(key, value)| (key.to_string(), value.to_str().unwrap().to_string())).collect();
|
||||
.filter_map(|(key, value)| {
|
||||
value.to_str().ok().map(|v| (key.to_string(), v.to_string()))
|
||||
})
|
||||
.collect();
|
||||
response_headers.push((axum::http::header::CONNECTION.as_str().to_string(), "keep-alive".to_string()));
|
||||
response_headers
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ impl ProviderConfig {
|
||||
}
|
||||
|
||||
fn notify_connection_change(&self, new_connections: usize) {
|
||||
self.connection_change_tx.send((self.name.clone(), new_connections)).unwrap();
|
||||
let _ = self.connection_change_tx.send((self.name.clone(), new_connections));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -4,18 +4,18 @@ use crate::api::model::active_user_manager::{ActiveUserManager, UserConnectionGu
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::stream::BoxedProviderStream;
|
||||
use crate::api::model::stream_error::StreamError;
|
||||
use crate::api::model::streams::timed_client_stream::TimedClientStream;
|
||||
use crate::api::model::streams::transport_stream_buffer::TransportStreamBuffer;
|
||||
use crate::model::{ProxyUserCredentials};
|
||||
use crate::model::ProxyUserCredentials;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use futures::StreamExt;
|
||||
use log::{error, info};
|
||||
use shared::model::UserConnectionPermission;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Poll, Waker};
|
||||
use crate::api::model::streams::timed_client_stream::TimedClientStream;
|
||||
use futures::{StreamExt};
|
||||
use shared::model::UserConnectionPermission;
|
||||
|
||||
const INNER_STREAM: u8 = 0_u8;
|
||||
const GRACE_BLOCK_STREAM: u8 = 1_u8;
|
||||
@@ -35,10 +35,10 @@ pub(in crate::api) struct ActiveClientStream {
|
||||
|
||||
impl ActiveClientStream {
|
||||
pub(crate) fn new(mut stream_details: StreamDetails,
|
||||
app_state: &AppState,
|
||||
user: &ProxyUserCredentials,
|
||||
connection_permission: UserConnectionPermission,
|
||||
addr: &str) -> Self {
|
||||
app_state: &AppState,
|
||||
user: &ProxyUserCredentials,
|
||||
connection_permission: UserConnectionPermission,
|
||||
addr: &str) -> Self {
|
||||
let active_user = app_state.active_users.clone();
|
||||
let active_provider = app_state.active_provider.clone();
|
||||
if connection_permission == UserConnectionPermission::Exhausted {
|
||||
@@ -59,15 +59,24 @@ impl ActiveClientStream {
|
||||
c.provider_connections_exhausted.clone()
|
||||
));
|
||||
|
||||
let stream = stream_details.stream.take().unwrap();
|
||||
let stream = match app_state.app_config.config.load().sleep_timer_mins {
|
||||
None => stream,
|
||||
Some(mins) => {
|
||||
let secs = u32::try_from((u64::from(mins) * 60).min(u64::from(u32::MAX))).unwrap_or(0);
|
||||
if secs > 0 {
|
||||
TimedClientStream::new(stream, secs).boxed()
|
||||
} else {
|
||||
stream
|
||||
let stream = match stream_details.stream.take() {
|
||||
None => {
|
||||
if let Some(guard) = stream_details.provider_connection_guard.as_ref() {
|
||||
guard.release();
|
||||
}
|
||||
futures::stream::empty::<Result<Bytes, StreamError>>().boxed()
|
||||
}
|
||||
Some(stream) => {
|
||||
match app_state.app_config.config.load().sleep_timer_mins {
|
||||
None => stream,
|
||||
Some(mins) => {
|
||||
let secs = u32::try_from((u64::from(mins) * 60).min(u64::from(u32::MAX))).unwrap_or(0);
|
||||
if secs > 0 {
|
||||
TimedClientStream::new(stream, secs).boxed()
|
||||
} else {
|
||||
stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -193,7 +202,7 @@ impl Stream for ActiveClientStream {
|
||||
};
|
||||
|
||||
if let Some(buffer) = buffer_opt {
|
||||
return Poll::Ready(Some(Ok(buffer.next_chunk())));
|
||||
return Poll::Ready(Some(Ok(buffer.next_chunk())));
|
||||
}
|
||||
|
||||
Poll::Ready(None)
|
||||
|
||||
@@ -7,6 +7,7 @@ use reqwest::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use crate::api::model::stream::ProviderStreamResponse;
|
||||
use crate::api::model::streams::transport_stream_buffer::TransportStreamBuffer;
|
||||
use crate::api::api_utils::try_unwrap_body;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum CustomVideoStreamType {
|
||||
@@ -65,7 +66,7 @@ pub fn create_custom_video_stream_response(config: &AppConfig, video_response: C
|
||||
for (key, value) in headers {
|
||||
builder = builder.header(key, value);
|
||||
}
|
||||
return builder.body(axum::body::Body::from_stream(stream)).unwrap().into_response();
|
||||
return try_unwrap_body!(builder.body(axum::body::Body::from_stream(stream)));
|
||||
}
|
||||
axum::http::StatusCode::FORBIDDEN.into_response()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user