refactored unwrap calls to make it more stable

This commit is contained in:
euzu
2025-07-22 11:32:46 +02:00
parent 7ef30e1aae
commit 6c238c607f
44 changed files with 560 additions and 479 deletions
+26 -22
View File
@@ -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(
+39 -30
View File
@@ -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,
+58 -47
View File
@@ -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()
}
}
+7 -11
View File
@@ -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()
}
+24 -25
View File
@@ -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();
+2 -1
View File
@@ -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()));
+9 -13
View File
@@ -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>> {
+12 -11
View File
@@ -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(),
};
}
+10 -12
View File
@@ -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)
}
+21 -17
View File
@@ -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() {
+13 -13
View File
@@ -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.
+16 -24
View File
@@ -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
View File
@@ -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;
}
+2 -2
View File
@@ -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())
}
}
+4 -1
View File
@@ -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
}
+1 -1
View File
@@ -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()
}