mirror of
https://github.com/euzu/tuliprox.git
synced 2026-09-28 03:52:25 +02:00
Replaced actix with axum
This commit is contained in:
Generated
+227
-578
File diff suppressed because it is too large
Load Diff
+6
-8
@@ -24,11 +24,10 @@ url = "2.5"
|
||||
reqwest = { version = "0", features = ["blocking", "json", "stream", "rustls-tls"] }
|
||||
chrono = "0.4"
|
||||
cron = "0.15"
|
||||
actix-web = "4.9"
|
||||
actix-files = "0"
|
||||
actix-cors = "0"
|
||||
actix-rt = "2.10"
|
||||
actix-web-httpauth = "0.8"
|
||||
axum = { version = "0" , features = ["macros", "default"]}
|
||||
axum-auth = "0"
|
||||
tower = "0"
|
||||
tower-http = { version = "0", features = ["cors", "auth", "fs"]}
|
||||
jsonwebtoken = "9.3"
|
||||
rust-argon2 = "2.1"
|
||||
futures = "0.3"
|
||||
@@ -49,13 +48,12 @@ flate2 = "1"
|
||||
blake3 = "1.5"
|
||||
bytes = "1.10"
|
||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||
tokio = "1.43"
|
||||
tokio = { version = "1.43", features = ["rt-multi-thread", "parking_lot", "fs"] }
|
||||
tokio-util = "0.7"
|
||||
paste = "1.0"
|
||||
tempfile = "3.16"
|
||||
ruzstd = "0"
|
||||
filetime = "0.2"
|
||||
parking_lot = "0.12"
|
||||
socket2 = "0.5"
|
||||
#[cfg(target_os = "macos")]
|
||||
libc = "0"
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
+134
-93
@@ -13,19 +13,16 @@ use crate::utils::file::file_utils::{create_new_file_for_write};
|
||||
use crate::tools::lru_cache::LRUResourceCache;
|
||||
use crate::utils::network::request;
|
||||
use crate::utils::network::request::sanitize_sensitive_info;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::body::{BodyStream, SizedStream};
|
||||
use actix_web::http::header::{HeaderValue, CACHE_CONTROL};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use parking_lot::Mutex;
|
||||
use futures::TryStreamExt;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use log::{error, log_enabled, trace};
|
||||
use reqwest::StatusCode;
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufWriter;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use std::sync::{Arc};
|
||||
use tokio::sync::Mutex;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::response::IntoResponse;
|
||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||
use url::Url;
|
||||
use crate::api::model::streams::active_client_stream::ActiveClientStream;
|
||||
@@ -38,14 +35,14 @@ macro_rules! try_option_bad_request {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);}
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
}
|
||||
};
|
||||
($option:expr) => {
|
||||
match $option {
|
||||
Some(value) => value,
|
||||
None => return HttpResponse::BadRequest().finish(),
|
||||
None => return axum::http::StatusCode::BAD_REQUEST.into_response(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -57,14 +54,14 @@ macro_rules! try_result_bad_request {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);}
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
}
|
||||
};
|
||||
($option:expr) => {
|
||||
match $option {
|
||||
Ok(value) => value,
|
||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||
Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -73,22 +70,32 @@ pub use try_option_bad_request;
|
||||
pub use try_result_bad_request;
|
||||
use crate::auth::authenticator::Claims;
|
||||
|
||||
pub async fn serve_file(file_path: &Path, req: &HttpRequest, mime_type: mime::Mime) -> HttpResponse {
|
||||
pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl axum::response::IntoResponse + Send {
|
||||
if file_path.exists() {
|
||||
if let Ok(file) = actix_files::NamedFile::open_async(file_path).await {
|
||||
let mut result = file.set_content_type(mime_type)
|
||||
.disable_content_disposition().into_response(req);
|
||||
let headers = result.headers_mut();
|
||||
headers.insert(CACHE_CONTROL, HeaderValue::from_bytes(b"no-cache").unwrap());
|
||||
return result;
|
||||
}
|
||||
return match tokio::fs::File::open(file_path).await {
|
||||
Ok(file) => {
|
||||
let reader = tokio::io::BufReader::new(file);
|
||||
let stream = tokio_util::io::ReaderStream::new(reader);
|
||||
let body = axum::body::Body::from_stream(stream);
|
||||
|
||||
axum::response::Response::builder()
|
||||
.status(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()
|
||||
}
|
||||
Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
};
|
||||
}
|
||||
HttpResponse::NoContent().finish()
|
||||
axum::http::StatusCode::NOT_FOUND.into_response()
|
||||
|
||||
}
|
||||
|
||||
pub async fn get_user_target_by_username<'a>(username: &str, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> {
|
||||
if !username.is_empty() {
|
||||
return app_state.config.get_target_for_username(username);
|
||||
return app_state.config.get_target_for_username(username).await;
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -96,13 +103,13 @@ pub async fn get_user_target_by_username<'a>(username: &str, app_state: &'a AppS
|
||||
pub async fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a UserApiRequest,
|
||||
app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> {
|
||||
if !username.is_empty() && !password.is_empty() {
|
||||
app_state.config.get_target_for_user(username, password)
|
||||
app_state.config.get_target_for_user(username, password).await
|
||||
} else {
|
||||
let token = api_req.token.as_str().trim();
|
||||
if token.is_empty() {
|
||||
None
|
||||
} else {
|
||||
app_state.config.get_target_for_user_by_token(token)
|
||||
app_state.config.get_target_for_user_by_token(token).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,26 +137,27 @@ fn get_stream_options(app_state: &AppState) -> (bool, bool, usize, bool) {
|
||||
(stream_retry, buffer_enabled, buffer_size, pipe_provider_stream)
|
||||
}
|
||||
|
||||
fn get_stream_content_length(provider_response: Option<&(Vec<(String, String)>, StatusCode)>) -> u64 {
|
||||
let content_length = provider_response
|
||||
.as_ref()
|
||||
.and_then(|(headers, _)| headers.iter().find(|(h, _)| h.eq(actix_web::http::header::CONTENT_LENGTH.as_str())))
|
||||
.and_then(|(_, val)| val.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
content_length
|
||||
}
|
||||
// fn get_stream_content_length(provider_response: Option<&(Vec<(String, String)>, StatusCode)>) -> u64 {
|
||||
// let content_length = provider_response
|
||||
// .as_ref()
|
||||
// .and_then(|(headers, _)| headers.iter().find(|(h, _)| h.eq(axum::http::header::CONTENT_LENGTH.as_str())))
|
||||
// .and_then(|(_, val)| val.parse::<u64>().ok())
|
||||
// .unwrap_or(0);
|
||||
// content_length
|
||||
// }
|
||||
|
||||
pub async fn stream_response(app_state: &AppState, stream_url: &str,
|
||||
req: &HttpRequest, input: Option<&ConfigInput>,
|
||||
req_headers: &HeaderMap,
|
||||
input: Option<&ConfigInput>,
|
||||
item_type: PlaylistItemType, target: &ConfigTarget,
|
||||
user: &ProxyUserCredentials) -> HttpResponse {
|
||||
user: &ProxyUserCredentials) -> impl axum::response::IntoResponse + Send {
|
||||
if log_enabled!(log::Level::Trace) { trace!("Try to open stream {}", sanitize_sensitive_info(stream_url)); }
|
||||
|
||||
let log_active_clients = app_state.config.log.as_ref().is_some_and(|l| l.active_clients);
|
||||
let share_stream = is_stream_share_enabled(item_type, target);
|
||||
if share_stream {
|
||||
if let Some(value) = shared_stream_response(app_state, stream_url, log_active_clients, user) {
|
||||
return value;
|
||||
if let Some(value) = shared_stream_response(app_state, stream_url, log_active_clients, user).await {
|
||||
return value.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,47 +167,65 @@ pub async fn stream_response(app_state: &AppState, stream_url: &str,
|
||||
if let Ok(url) = Url::parse(stream_url) {
|
||||
let active_clients = Arc::clone(&app_state.active_users);
|
||||
let (stream_opt, provider_response) = if direct_pipe_provider_stream {
|
||||
provider_stream::get_provider_pipe_stream(&app_state.config, &app_state.http_client, &url, req, input, item_type).await
|
||||
provider_stream::get_provider_pipe_stream(&app_state.config, &app_state.http_client, &url, req_headers, input, item_type).await
|
||||
} else {
|
||||
let buffer_stream_options = BufferStreamOptions::new(item_type, stream_retry, buffer_enabled, buffer_size, share_stream);
|
||||
provider_stream::get_provider_reconnect_buffered_stream(&app_state.config, &app_state.http_client, &url, req, input, buffer_stream_options).await
|
||||
provider_stream::get_provider_reconnect_buffered_stream(&app_state.config, &app_state.http_client, &url, req_headers, input, buffer_stream_options).await
|
||||
};
|
||||
if let Some(stream) = stream_opt {
|
||||
let content_length = get_stream_content_length(provider_response.as_ref());
|
||||
let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients);
|
||||
// let content_length = get_stream_content_length(provider_response.as_ref());
|
||||
let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients).await;
|
||||
let stream_resp = if share_stream {
|
||||
let shared_headers = provider_response.as_ref().map_or_else(Vec::new, |(h, _)| h.clone());
|
||||
SharedStreamManager::subscribe(app_state, stream_url, stream, shared_headers, buffer_size);
|
||||
if let Some(broadcast_stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url) {
|
||||
let mut response_builder = get_stream_response_with_headers(provider_response, stream_url);
|
||||
if content_length > 0 {
|
||||
response_builder.body(SizedStream::new(content_length, broadcast_stream)) }
|
||||
else {
|
||||
response_builder.body(BodyStream::new(broadcast_stream))
|
||||
SharedStreamManager::subscribe(app_state, stream_url, stream, shared_headers, buffer_size).await;
|
||||
if let Some(broadcast_stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url).await {
|
||||
let (status_code, header_map) = get_stream_response_with_headers(provider_response, stream_url);
|
||||
let mut response = axum::response::Response::builder()
|
||||
.status(status_code);
|
||||
for (key, value) in &header_map {
|
||||
response = response.header(key, value);
|
||||
}
|
||||
response.body(axum::body::Body::from_stream(broadcast_stream)).unwrap().into_response()
|
||||
// if content_length > 0 {
|
||||
// response_builder.body(SizedStream::new(content_length, broadcast_stream)) }
|
||||
// else {
|
||||
// response_builder.body(BodyStream::new(broadcast_stream))
|
||||
// }
|
||||
} else {
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
} else {
|
||||
let mut response_builder = get_stream_response_with_headers(provider_response, stream_url);
|
||||
if content_length > 0 { response_builder.body(SizedStream::new(content_length, stream)) } else { response_builder.streaming(stream) }
|
||||
let (status_code, header_map) = get_stream_response_with_headers(provider_response, stream_url);
|
||||
let mut response = axum::response::Response::builder()
|
||||
.status(status_code);
|
||||
for (key, value) in &header_map {
|
||||
response = response.header(key, value);
|
||||
}
|
||||
response.body(axum::body::Body::from_stream(stream)).unwrap().into_response()
|
||||
|
||||
// if content_length > 0 { response_builder.body(SizedStream::new(content_length, stream)) } else { response_builder.streaming(stream) }
|
||||
};
|
||||
|
||||
return stream_resp;
|
||||
return stream_resp.into_response();
|
||||
}
|
||||
}
|
||||
error!("Cant open stream {}", sanitize_sensitive_info(stream_url));
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
|
||||
fn shared_stream_response(app_state: &AppState, stream_url: &str, log_active_clients: bool, user: &ProxyUserCredentials) -> Option<HttpResponse> {
|
||||
if let Some(stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url) {
|
||||
async fn shared_stream_response(app_state: &AppState, stream_url: &str, log_active_clients: bool, user: &ProxyUserCredentials) -> Option<impl IntoResponse> {
|
||||
if let Some(stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url).await {
|
||||
debug_if_enabled!("Using shared channel {}", sanitize_sensitive_info(stream_url));
|
||||
if let Some(headers) = app_state.shared_stream_manager.get_shared_state_headers(stream_url) {
|
||||
let mut response_builder = get_stream_response_with_headers(Some((headers.clone(), StatusCode::OK)), stream_url);
|
||||
if let Some(headers) = app_state.shared_stream_manager.get_shared_state_headers(stream_url).await {
|
||||
let (status_code, header_map) = get_stream_response_with_headers(Some((headers.clone(), StatusCode::OK)), stream_url);
|
||||
let active_clients = Arc::clone(&app_state.active_users);
|
||||
let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients);
|
||||
return Some(response_builder.body(BodyStream::new(stream)));
|
||||
let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients).await.boxed();
|
||||
let mut response = axum::response::Response::builder()
|
||||
.status(status_code);
|
||||
for (key, value) in &header_map {
|
||||
response = response.header(key, value);
|
||||
}
|
||||
return Some(response.body(axum::body::Body::from_stream(stream)).unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -209,9 +235,9 @@ pub fn is_stream_share_enabled(item_type: PlaylistItemType, target: &ConfigTarge
|
||||
(item_type == PlaylistItemType::Live || item_type == PlaylistItemType::LiveHls) && target.options.as_ref().is_some_and(|opt| opt.share_live_streams)
|
||||
}
|
||||
|
||||
pub type HeaderFilter = Option<Box<dyn Fn(&str) -> bool>>;
|
||||
pub fn get_headers_from_request(req: &HttpRequest, filter: &HeaderFilter) -> HashMap<String, Vec<u8>> {
|
||||
req.headers()
|
||||
pub type HeaderFilter = Option<Box<dyn Fn(&str) -> bool + Send>>;
|
||||
pub fn get_headers_from_request(req_headers: &HeaderMap, filter: &HeaderFilter) -> HashMap<String, Vec<u8>> {
|
||||
req_headers
|
||||
.iter()
|
||||
.filter(|(k, _)| match &filter {
|
||||
None => true,
|
||||
@@ -221,36 +247,34 @@ pub fn get_headers_from_request(req: &HttpRequest, filter: &HeaderFilter) -> Has
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_add_cache_content(res_url: &str, cache: &Arc<Option<Mutex<LRUResourceCache>>>) -> Box<dyn Fn(usize)> {
|
||||
fn get_add_cache_content(res_url: &str, cache: &Arc<Option<Mutex<LRUResourceCache>>>) -> Arc<dyn Fn(usize) + Send + Sync> {
|
||||
let resource_url = String::from(res_url);
|
||||
let cache = Arc::clone(cache);
|
||||
let add_cache_content: Box<dyn Fn(usize)> = Box::new(move |size| {
|
||||
let add_cache_content: Arc<dyn Fn(usize) + Send + Sync> = Arc::new(move |size| {
|
||||
let res_url = resource_url.clone();
|
||||
let cache = Arc::clone(&cache);
|
||||
actix_rt::spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
if let Some(cache) = cache.as_ref() {
|
||||
let mut guard = cache.lock();
|
||||
let _ = guard.add_content(&res_url, size);
|
||||
let _ = cache.lock().await.add_content(&res_url, size);
|
||||
}
|
||||
});
|
||||
});
|
||||
add_cache_content
|
||||
}
|
||||
|
||||
pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &HttpRequest, input: Option<&ConfigInput>) -> HttpResponse {
|
||||
pub async fn resource_response(app_state: &AppState, resource_url: &str, req_headers: &HeaderMap, input: Option<&ConfigInput>) -> impl axum::response::IntoResponse + Send {
|
||||
if resource_url.is_empty() {
|
||||
return HttpResponse::NoContent().finish();
|
||||
return axum::http::StatusCode::NO_CONTENT.into_response();
|
||||
}
|
||||
let filter: HeaderFilter = Some(Box::new(|key| key != "if-none-match" && key != "if-modified-since"));
|
||||
let req_headers = get_headers_from_request(req, &filter);
|
||||
let req_headers = get_headers_from_request(req_headers, &filter);
|
||||
if let Some(cache) = app_state.cache.as_ref() {
|
||||
let mut guard = cache.lock();
|
||||
let mut guard = cache.lock().await;
|
||||
if let Some(resource_path) = guard.get_content(resource_url) {
|
||||
if let Ok(named_file) = NamedFile::open(resource_path) {
|
||||
trace_if_enabled!("Responding resource from cache {}", sanitize_sensitive_info(resource_url));
|
||||
return named_file.into_response(req);
|
||||
}
|
||||
trace_if_enabled!("Responding resource from cache {}", sanitize_sensitive_info(resource_url));
|
||||
return serve_file(&resource_path, mime::APPLICATION_OCTET_STREAM).await.into_response();
|
||||
}
|
||||
|
||||
}
|
||||
trace_if_enabled!("Try to fetch resource {}", sanitize_sensitive_info(resource_url));
|
||||
if let Ok(url) = Url::parse(resource_url) {
|
||||
@@ -259,24 +283,23 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &H
|
||||
Ok(response) => {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
let mut response_builder = HttpResponse::Ok();
|
||||
response.headers().iter().for_each(|(k, v)| {
|
||||
response_builder.insert_header((k.as_str(), v.as_ref()));
|
||||
});
|
||||
let mut response_builder = axum::response::Response::builder()
|
||||
.status(StatusCode::OK);
|
||||
for (key, value) in response.headers() {
|
||||
response_builder = response_builder.header(key, value);
|
||||
}
|
||||
|
||||
let byte_stream = response.bytes_stream().map_err(|err| StreamError::reqwest(&err));
|
||||
if let Some(cache) = app_state.cache.as_ref() {
|
||||
let resource_path = {
|
||||
cache.lock().store_path(resource_url)
|
||||
};
|
||||
let resource_path = cache.lock().await.store_path(resource_url);
|
||||
if let Ok(file) = create_new_file_for_write(&resource_path) {
|
||||
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(BodyStream::new(stream));
|
||||
return response_builder.body(axum::body::Body::from_stream(stream)).unwrap().into_response();
|
||||
}
|
||||
}
|
||||
return response_builder.body(BodyStream::new(byte_stream));
|
||||
return response_builder.body(axum::body::Body::from_stream(byte_stream)).unwrap().into_response();
|
||||
}
|
||||
debug_if_enabled!("Failed to open resource got status {} for {}", status, sanitize_sensitive_info(resource_url));
|
||||
}
|
||||
@@ -287,7 +310,7 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &H
|
||||
} else {
|
||||
error!("Url is malformed {}", sanitize_sensitive_info(resource_url));
|
||||
}
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
|
||||
pub fn separate_number_and_remainder(input: &str) -> (String, Option<String>) {
|
||||
@@ -298,18 +321,36 @@ pub fn separate_number_and_remainder(input: &str) -> (String, Option<String>) {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn empty_json_list_response() -> HttpResponse {
|
||||
HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("[]")
|
||||
pub fn empty_json_list_response() -> impl axum::response::IntoResponse + Send {
|
||||
axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", mime::APPLICATION_JSON.to_string())
|
||||
.body("[]".to_string())
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub fn get_username_from_auth_header(credentials: Option<BearerAuth>, app_state: &web::Data<Arc<AppState>>) -> Option<String> {
|
||||
if let Some(bearer) = credentials {
|
||||
if let Some(web_auth_config) = app_state.config.web_auth.as_ref() {
|
||||
let secret_key = web_auth_config.secret.as_ref();
|
||||
if let Ok(token_data) = decode::<Claims>(bearer.token(), &DecodingKey::from_secret(secret_key), &Validation::new(Algorithm::HS256)) {
|
||||
return Some(token_data.claims.username);
|
||||
}
|
||||
pub fn get_username_from_auth_header(
|
||||
token: &str,
|
||||
app_state: &Arc<AppState>,
|
||||
) -> Option<String> {
|
||||
if let Some(web_auth_config) = &app_state.config.web_auth {
|
||||
let secret_key: &str = web_auth_config.secret.as_ref();
|
||||
if let Ok(token_data) = decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(secret_key.as_bytes()),
|
||||
&Validation::new(Algorithm::HS256),
|
||||
) {
|
||||
return Some(token_data.claims.username);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn redirect(url: &str) -> impl IntoResponse {
|
||||
axum::response::Response::builder()
|
||||
.status(StatusCode::FOUND)
|
||||
.header("Location", url)
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap()
|
||||
}
|
||||
@@ -2,7 +2,6 @@ use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::download::{DownloadQueue, FileDownload, FileDownloadRequest};
|
||||
use crate::model::config::VideoDownloadConfig;
|
||||
use crate::utils::network::request;
|
||||
use actix_web::{web, HttpResponse};
|
||||
use tokio::sync::RwLock;
|
||||
use futures::stream::TryStreamExt;
|
||||
use log::info;
|
||||
@@ -12,6 +11,7 @@ use std::io::{Write};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::{fs};
|
||||
use axum::response::IntoResponse;
|
||||
use crate::m3u_filter_error::to_io_error;
|
||||
|
||||
async fn download_file(active: Arc<RwLock<Option<FileDownload>>>, client: &reqwest::Client) -> Result<(), String> {
|
||||
@@ -69,7 +69,7 @@ async fn run_download_queue(download_cfg: &VideoDownloadConfig, download_queue:
|
||||
let dq = Arc::clone(download_queue);
|
||||
match reqwest::Client::builder().default_headers(headers).build() {
|
||||
Ok(client) => {
|
||||
actix_rt::spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if dq.active.read().await.deref().is_some() {
|
||||
match download_file(Arc::clone(&dq.active), &client).await {
|
||||
@@ -110,41 +110,40 @@ macro_rules! download_info {
|
||||
}
|
||||
|
||||
pub async fn queue_download_file(
|
||||
req: web::Json<FileDownloadRequest>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(req): axum::extract::Json<FileDownloadRequest>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
if let Some(download_cfg) = &app_state.config.video.as_ref().unwrap().download {
|
||||
if download_cfg.directory.is_none() {
|
||||
return HttpResponse::BadRequest().json(json!({"error": "Server config missing video.download.directory configuration"}));
|
||||
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) => {
|
||||
let response = HttpResponse::Ok().json(download_info!(file_download));
|
||||
app_state.downloads.queue.lock().await.push_back(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(download_cfg, &app_state.downloads).await {
|
||||
Ok(()) => {}
|
||||
Err(err) => return HttpResponse::InternalServerError().json(json!({"error": err})),
|
||||
Err(err) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err}))).into_response(),
|
||||
}
|
||||
}
|
||||
response
|
||||
axum::Json(download_info!(&file_download)).into_response()
|
||||
}
|
||||
None => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})),
|
||||
None => (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest().json(json!({"error": "Server config missing video.download configuration"}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download configuration"}))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_file_info(
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let finished_list: &[Value] = &app_state.downloads.finished.write().await.drain(..)
|
||||
.map(|fd| download_info!(fd)).collect::<Vec<Value>>();
|
||||
|
||||
(*app_state.downloads.active.read().await).as_ref().map_or_else(|| HttpResponse::Ok().json(json!({
|
||||
(*app_state.downloads.active.read().await).as_ref().map_or_else(|| axum::Json(json!({
|
||||
"completed": true, "downloads": finished_list
|
||||
})), |file_download| HttpResponse::Ok().json(json!({
|
||||
})), |file_download| axum::Json(json!({
|
||||
"completed": false, "downloads": finished_list, "active": download_info!(file_download)
|
||||
})))
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
use crate::api::model::app_state::HdHomerunAppState;
|
||||
use axum::response::IntoResponse;
|
||||
use crate::api::model::app_state::{HdHomerunAppState};
|
||||
use crate::model::api_proxy::{ProxyType, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, TargetType};
|
||||
use crate::model::playlist::{M3uPlaylistItem, XtreamCluster, XtreamPlaylistItem};
|
||||
use crate::processing::parser::xtream::get_xtream_url;
|
||||
use crate::utils::json_utils::get_string_from_serde_value;
|
||||
// https://info.hdhomerun.com/info/http_api
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use bytes::Bytes;
|
||||
use futures::{stream, Stream, StreamExt};
|
||||
use log::{error, warn};
|
||||
@@ -14,6 +13,9 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json};
|
||||
use crate::repository::m3u_playlist_iterator::M3uPlaylistIterator;
|
||||
use crate::repository::xtream_playlist_iterator::{XtreamPlaylistIterator};
|
||||
|
||||
// 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];
|
||||
// const RESPONSE_BYTES: &[u8] = &[0, 3, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143];
|
||||
|
||||
@@ -154,9 +156,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn create_device(app_state: &web::Data<HdHomerunAppState>) -> Option<Device> {
|
||||
if let Some(credentials) = app_state.app_state.config.get_user_credentials(&app_state.device.t_username) {
|
||||
let server_info = app_state.app_state.config.get_user_server_info(&credentials);
|
||||
async fn create_device(app_state: &Arc<HdHomerunAppState>) -> Option<Device> {
|
||||
if let Some(credentials) = app_state.app_state.config.get_user_credentials(&app_state.device.t_username).await {
|
||||
let server_info = app_state.app_state.config.get_user_server_info(&credentials).await;
|
||||
let device = &app_state.device;
|
||||
let device_url = format!("{}://{}:{}", server_info.protocol, server_info.host, device.port);
|
||||
Some(Device {
|
||||
@@ -180,36 +182,37 @@ fn create_device(app_state: &web::Data<HdHomerunAppState>) -> Option<Device> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn device_xml(app_state: web::Data<HdHomerunAppState>) -> impl Responder {
|
||||
if let Some(device) = create_device(&app_state) {
|
||||
HttpResponse::Ok().content_type("application/xml").body(device.as_xml())
|
||||
async fn device_xml(axum::extract::State(app_state): axum::extract::State<Arc<HdHomerunAppState>>) -> impl IntoResponse {
|
||||
if let Some(device) = create_device(&app_state).await {
|
||||
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()
|
||||
} else {
|
||||
HttpResponse::InternalServerError().finish()
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn device_json(app_state: web::Data<HdHomerunAppState>) -> impl Responder {
|
||||
if let Some(device) = create_device(&app_state) {
|
||||
HttpResponse::Ok().json(device)
|
||||
async fn device_json(axum::extract::State(app_state): axum::extract::State<Arc<HdHomerunAppState>>) -> impl IntoResponse {
|
||||
if let Some(device) = create_device(&app_state).await {
|
||||
axum::Json(device).into_response()
|
||||
} else {
|
||||
HttpResponse::InternalServerError().finish()
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn discover_json(app_state: web::Data<HdHomerunAppState>) -> impl Responder {
|
||||
if let Some(device) = create_device(&app_state) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(device)
|
||||
async fn discover_json(axum::extract::State(app_state): axum::extract::State<Arc<HdHomerunAppState>>) -> impl IntoResponse {
|
||||
if let Some(device) = create_device(&app_state).await {
|
||||
axum::Json(device).into_response()
|
||||
} else {
|
||||
HttpResponse::InternalServerError().finish()
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn lineup_status() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.json(json!({
|
||||
async fn lineup_status() -> impl IntoResponse {
|
||||
axum::Json(json!({
|
||||
"ScanInProgress": 0,
|
||||
"ScanPossible": 0,
|
||||
"Source": "Cable",
|
||||
@@ -217,9 +220,10 @@ async fn lineup_status() -> impl Responder {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn lineup_json(app_state: web::Data<HdHomerunAppState>) -> impl Responder {
|
||||
#[axum::debug_handler]
|
||||
async fn lineup_json(axum::extract::State(app_state): axum::extract::State<Arc<HdHomerunAppState>>) -> impl IntoResponse {
|
||||
let cfg = Arc::clone(&app_state.app_state.config);
|
||||
if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username) {
|
||||
if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username).await {
|
||||
let use_output = target.get_hdhomerun_output().as_ref().and_then(|o| o.use_output.clone());
|
||||
let use_all = use_output.is_none();
|
||||
let use_m3u = use_output.as_ref() == Some(&TargetType::M3u);
|
||||
@@ -230,11 +234,13 @@ async fn lineup_json(app_state: web::Data<HdHomerunAppState>) -> impl Responder
|
||||
let body_stream = stream::once(async { Ok(Bytes::from("[")) })
|
||||
.chain(stream)
|
||||
.chain(stream::once(async { Ok(Bytes::from("]")) }));
|
||||
return HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.streaming(body_stream);
|
||||
return 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();
|
||||
} else if (use_all || use_xtream) && target.has_output(&TargetType::Xtream) {
|
||||
let server_info = app_state.app_state.config.get_user_server_info(&credentials);
|
||||
let server_info = app_state.app_state.config.get_user_server_info(&credentials).await;
|
||||
let base_url = if credentials.proxy == ProxyType::Reverse {
|
||||
Some(server_info.get_base_url())
|
||||
} else {
|
||||
@@ -253,30 +259,34 @@ async fn lineup_json(app_state: web::Data<HdHomerunAppState>) -> impl Responder
|
||||
.chain(stream::once(async { Ok(Bytes::from(",")) }))
|
||||
.chain(vod_stream)
|
||||
.chain(stream::once(async { Ok(Bytes::from("]")) }));
|
||||
return HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.streaming(body_stream);
|
||||
return 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();
|
||||
}
|
||||
}
|
||||
HttpResponse::NotFound().finish()
|
||||
axum::http::StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
|
||||
async fn auto_channel(_app_state: web::Data<HdHomerunAppState>, path: web::Path<String>) -> impl Responder {
|
||||
let channel = path.into_inner();
|
||||
async fn auto_channel(axum::extract::State(_app_state): axum::extract::State<Arc<HdHomerunAppState>>,
|
||||
axum::extract::Path(channel): axum::extract::Path<String>) -> impl IntoResponse {
|
||||
warn!("HdHomerun api not implemented for auto_channel {channel}");
|
||||
HttpResponse::NotFound().finish()
|
||||
axum::http::StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
|
||||
pub fn hdhr_api_register(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("/device.xml").route(web::get().to(device_xml)));
|
||||
cfg.service(web::resource("/device.json").route(web::get().to(device_json)));
|
||||
cfg.service(web::resource("/discover.json").route(web::get().to(discover_json)));
|
||||
cfg.service(web::resource("/lineup_status.json").route(web::get().to(lineup_status)));
|
||||
cfg.service(web::resource("/lineup.json").route(web::get().to(lineup_json)));
|
||||
pub fn hdhr_api_register() -> axum::Router<Arc<HdHomerunAppState>> {
|
||||
axum::Router::new()
|
||||
.route("/device.xml", axum::routing::get(device_xml))
|
||||
.route("/device.json", axum::routing::get(device_json))
|
||||
.route("/discover.json", axum::routing::get(discover_json))
|
||||
.route("/lineup_status.json", axum::routing::get(lineup_status))
|
||||
.route("/lineup.json", axum::routing::get(lineup_json))
|
||||
// cfg.service(web::resource("/lineup.xml").route(web::get().to(lineup_xml)));
|
||||
// cfg.service(web::resource("/lineup.m3u").route(web::get().to(lineup_m3u)));
|
||||
cfg.service(web::resource("/auto/{channel}").route(web::get().to(auto_channel)));
|
||||
cfg.service(web::resource("/tuner{tuner_num}/{channel}").route(web::get().to(auto_channel)));
|
||||
.route("/auto/{channel}", axum::routing::get(auto_channel))
|
||||
.route("/tuner{tuner_num}/{channel}", axum::routing::get(auto_channel))
|
||||
}
|
||||
|
||||
// fn start_hdhomerum_discovery_handler(ssdp_socket: Arc<UdpSocket>, server: String, location: String, cache_control: String, usn: String) {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_web::web::Data;
|
||||
use log::{debug, error};
|
||||
use serde::Deserialize;
|
||||
use crate::api::api_utils::{get_user_target_by_credentials, stream_response};
|
||||
use crate::api::api_utils::{try_option_bad_request, try_result_bad_request};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::model::api_proxy::ProxyUserCredentials;
|
||||
use crate::model::config::{ConfigInput, TargetType};
|
||||
use crate::model::playlist::{PlaylistEntry, PlaylistItemType, XtreamCluster};
|
||||
use crate::processing::parser::hls::{rewrite_hls, M3U_HLSR_PREFIX};
|
||||
use crate::api::api_utils::{try_option_bad_request, try_result_bad_request};
|
||||
use crate::repository::{m3u_repository, xtream_repository};
|
||||
use crate::repository::playlist_repository::HLS_EXT;
|
||||
use crate::repository::{m3u_repository, xtream_repository};
|
||||
use crate::utils::network::request;
|
||||
use crate::utils::network::request::{replace_extension, sanitize_sensitive_info};
|
||||
use axum::response::IntoResponse;
|
||||
use log::{debug, error};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
@@ -27,40 +26,44 @@ struct HlsApiPathParams {
|
||||
chunk: String,
|
||||
}
|
||||
|
||||
pub(in crate::api) async fn handle_hls_stream_request(app_state: &Data<Arc<AppState>>, user: &ProxyUserCredentials, pli: &dyn PlaylistEntry, input: &ConfigInput, target_type: TargetType) -> HttpResponse {
|
||||
pub(in crate::api) async fn handle_hls_stream_request(app_state: &Arc<AppState>, user: &ProxyUserCredentials, pli: &dyn PlaylistEntry, input: &ConfigInput, target_type: TargetType) -> impl axum::response::IntoResponse + Send {
|
||||
let url = replace_extension(&pli.get_provider_url(), HLS_EXT);
|
||||
match request::download_text_content(Arc::clone(&app_state.http_client), input, &url, None).await {
|
||||
Ok(content) => {
|
||||
let hls_content = rewrite_hls(&content, pli.get_virtual_id(), user, &target_type);
|
||||
HttpResponse::Ok().content_type("application/x-mpegurl").body(hls_content)
|
||||
axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, "application/x-mpegurl")
|
||||
.body(hls_content)
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to download m3u8 {}", sanitize_sensitive_info(err.to_string().as_str()));
|
||||
HttpResponse::NoContent().finish()
|
||||
axum::http::StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn hls_api_stream(
|
||||
req: &HttpRequest,
|
||||
api_req: &web::Query<UserApiRequest>,
|
||||
path: web::Path<HlsApiPathParams>,
|
||||
app_state: &web::Data<Arc<AppState>>,
|
||||
target_type: TargetType
|
||||
) -> HttpResponse {
|
||||
let params = path.into_inner();
|
||||
req_headers: &axum::http::HeaderMap,
|
||||
api_req: &UserApiRequest,
|
||||
params: HlsApiPathParams,
|
||||
app_state: &Arc<AppState>,
|
||||
target_type: TargetType,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let (user, target) = try_option_bad_request!(
|
||||
get_user_target_by_credentials(¶ms.username, ¶ms.password, api_req, app_state).await,
|
||||
false,
|
||||
format!("Could not find any user {}", params.username));
|
||||
if !user.has_permissions(app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
let target_name = &target.name;
|
||||
let virtual_id: u32 = try_result_bad_request!(params.channel.parse());
|
||||
let (pli_url, input_name) = if target_type == TargetType::Xtream {
|
||||
let (pli, _ ) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id));
|
||||
let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id));
|
||||
(pli.url, pli.input_name)
|
||||
} else {
|
||||
let pli = try_result_bad_request!(m3u_repository::m3u_get_item_for_stream_id(virtual_id, &app_state.config, target).await, true, format!("Failed to read xtream item for stream id {}", virtual_id));
|
||||
@@ -74,30 +77,33 @@ async fn hls_api_stream(
|
||||
// we don't respond as hlsr, we take the original stream, because the location could be different and then it does not work
|
||||
// The next problem is, different url to same channel causes to fail stream share.
|
||||
// let stream_url = format!("{input_url}/hlsr/{token}/{input_username}/{input_password}/{}/{hash}/{chunk}", pli.provider_id);
|
||||
stream_response(app_state, &pli_url, req, Some(input), PlaylistItemType::Live, target, &user).await
|
||||
stream_response(app_state, &pli_url, req_headers, Some(input), PlaylistItemType::Live, target, &user).await.into_response()
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn hls_api_stream_xtream(
|
||||
req: HttpRequest,
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
path: web::Path<HlsApiPathParams>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
hls_api_stream(&req, &api_req, path, &app_state, TargetType::Xtream).await
|
||||
req_headers: axum::http::HeaderMap,
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
axum::extract::Path(params): axum::extract::Path<HlsApiPathParams>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
hls_api_stream(&req_headers, &api_req, params, &app_state, TargetType::Xtream).await.into_response()
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn hls_api_stream_m3u(
|
||||
req: HttpRequest,
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
path: web::Path<HlsApiPathParams>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
hls_api_stream(&req, &api_req, path, &app_state, TargetType::M3u).await
|
||||
req_headers: axum::http::HeaderMap,
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
axum::extract::Path(params): axum::extract::Path<HlsApiPathParams>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
hls_api_stream(&req_headers, &api_req, params, &app_state, TargetType::M3u).await.into_response()
|
||||
}
|
||||
|
||||
pub fn hls_api_register(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("/hlsr/{token}/{username}/{password}/{channel}/{hash}/{chunk}").route(web::get().to(hls_api_stream_xtream)));
|
||||
cfg.service(web::resource(format!("/{M3U_HLSR_PREFIX}/{{token}}/{{username}}/{{password}}/{{channel}}/{{hash}}/{{chunk}}")).route(web::get().to(hls_api_stream_m3u)));
|
||||
pub fn hls_api_register() -> axum::Router<Arc<AppState>> {
|
||||
axum::Router::new()
|
||||
.route("/hlsr/{token}/{username}/{password}/{channel}/{hash}/{chunk}", axum::routing::get(hls_api_stream_xtream))
|
||||
.route(&format!("/{M3U_HLSR_PREFIX}/{{token}}/{{username}}/{{password}}/{{channel}}/{{hash}}/{{chunk}}"), axum::routing::get(hls_api_stream_m3u))
|
||||
//cfg.service(web::resource("/hls/{token}/{stream}").route(web::get().to(xtream_player_api_hls_stream)));
|
||||
//cfg.service(web::resource("/play/{token}/{type}").route(web::get().to(xtream_player_api_play_stream)));
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use bytes::Bytes;
|
||||
use futures::stream;
|
||||
use log::{debug, error};
|
||||
|
||||
use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, resource_response,
|
||||
separate_number_and_remainder, stream_response, try_option_bad_request,
|
||||
try_result_bad_request};
|
||||
use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, redirect, resource_response, separate_number_and_remainder, stream_response, try_option_bad_request, try_result_bad_request};
|
||||
use crate::api::endpoints::hls_api::handle_hls_stream_request;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
@@ -15,73 +7,84 @@ use crate::model::config::TargetType;
|
||||
use crate::model::playlist::{FieldGetAccessor, XtreamCluster};
|
||||
use crate::repository::m3u_playlist_iterator::{M3U_RESOURCE_PATH, M3U_STREAM_PATH};
|
||||
use crate::repository::m3u_repository::{m3u_get_item_for_stream_id, m3u_load_rewrite_playlist};
|
||||
use crate::utils::network::request::{replace_extension, sanitize_sensitive_info};
|
||||
use crate::utils::{debug_if_enabled};
|
||||
use crate::repository::playlist_repository::HLS_EXT;
|
||||
use crate::utils::network::request::{replace_extension, sanitize_sensitive_info};
|
||||
use crate::utils::debug_if_enabled;
|
||||
use axum::response::IntoResponse;
|
||||
use bytes::Bytes;
|
||||
use futures::stream;
|
||||
use log::{debug, error};
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn m3u_api(
|
||||
api_req: &UserApiRequest,
|
||||
app_state: &AppState,
|
||||
) -> HttpResponse {
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
match get_user_target(api_req, app_state).await {
|
||||
Some((user, target)) => {
|
||||
match m3u_load_rewrite_playlist(&app_state.config, target, &user).await {
|
||||
Ok(m3u_iter) => {
|
||||
// Convert the iterator into a stream of `Bytes`
|
||||
let content_stream = stream::iter(m3u_iter.map(|line| Ok::<Bytes, String>(Bytes::from([line.to_string().as_bytes(), b"\n"].concat()))));
|
||||
let mut builder = HttpResponse::Ok();
|
||||
builder.content_type(mime::TEXT_PLAIN_UTF_8);
|
||||
|
||||
let mut builder = axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::TEXT_PLAIN_UTF_8.to_string());
|
||||
if api_req.content_type == "m3u_plus" {
|
||||
builder.insert_header(("Content-Disposition", "attachment; filename=\"playlist.m3u\""));
|
||||
builder = builder.header("Content-Disposition", "attachment; filename=\"playlist.m3u\"");
|
||||
}
|
||||
builder.streaming(content_stream)
|
||||
builder.body(axum::body::Body::from_stream(content_stream)).unwrap().into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{}", sanitize_sensitive_info(err.to_string().as_str()));
|
||||
HttpResponse::NoContent().finish()
|
||||
axum::http::StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => HttpResponse::BadRequest().finish(),
|
||||
None => axum::http::StatusCode::BAD_REQUEST.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn m3u_api_get(api_req: web::Query<UserApiRequest>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
m3u_api(&api_req.into_inner(), &app_state).await
|
||||
}
|
||||
async fn m3u_api_post(
|
||||
api_req: web::Form<UserApiRequest>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
m3u_api(&api_req.into_inner(), &app_state).await
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn m3u_api_get(axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
m3u_api(&api_req, &app_state).await
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn m3u_api_post(
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Form(api_req): axum::extract::Form<UserApiRequest>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
m3u_api(&api_req, &app_state).await.into_response()
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn m3u_api_stream(
|
||||
req: HttpRequest,
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
path: web::Path<(String, String, String)>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let (username, password, stream_id) = path.into_inner();
|
||||
req_headers: axum::http::HeaderMap,
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
axum::extract::Path((username, password, stream_id)): axum::extract::Path<(String, String, String)>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let (action_stream_id, stream_ext) = separate_number_and_remainder(&stream_id);
|
||||
let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse());
|
||||
let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state).await
|
||||
else { return HttpResponse::BadRequest().finish() };
|
||||
if !user.has_permissions(&app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
else { return axum::http::StatusCode::BAD_REQUEST.into_response() };
|
||||
if !user.has_permissions(&app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
if !target.has_output(&TargetType::M3u) {
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
|
||||
let m3u_item = match m3u_get_item_for_stream_id(virtual_id, &app_state.config, target).await {
|
||||
Ok(item) => item,
|
||||
Err(err) => {
|
||||
error!("Failed to get m3u url: {}", sanitize_sensitive_info(err.to_string().as_str()));
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,77 +94,82 @@ async fn m3u_api_stream(
|
||||
let redirect_url = if is_hls_request { &replace_extension(&m3u_item.url, "m3u8") } else { &m3u_item.url };
|
||||
// TODO alias processing
|
||||
debug_if_enabled!("Redirecting m3u stream request to {}", sanitize_sensitive_info(redirect_url));
|
||||
return HttpResponse::Found().insert_header(("Location", redirect_url.as_str())).finish();
|
||||
return redirect(redirect_url.as_str()).into_response();
|
||||
}
|
||||
// Reverse proxy mode
|
||||
if is_hls_request {
|
||||
let target_name = &target.name;
|
||||
let input = try_option_bad_request!(app_state.config.get_input_by_name(m3u_item.input_name.as_str()), true,
|
||||
format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", XtreamCluster::Live));
|
||||
return handle_hls_stream_request(&app_state, &user, &m3u_item, input, TargetType::M3u).await;
|
||||
return handle_hls_stream_request(&app_state, &user, &m3u_item, input, TargetType::M3u).await.into_response();
|
||||
}
|
||||
|
||||
stream_response(&app_state, m3u_item.url.as_str(), &req, None, m3u_item.item_type, target, &user).await
|
||||
stream_response(&app_state, m3u_item.url.as_str(), &req_headers, None, m3u_item.item_type, target, &user).await.into_response()
|
||||
}
|
||||
|
||||
async fn m3u_api_resource(
|
||||
req: HttpRequest,
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
path: web::Path<(String, String, String, String)>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let (username, password, stream_id, resource) = path.into_inner();
|
||||
let Ok(m3u_stream_id) = stream_id.parse::<u32>() else { return HttpResponse::BadRequest().finish() };
|
||||
req_headers: axum::http::HeaderMap,
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<(String, String, String, String)>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let Ok(m3u_stream_id) = stream_id.parse::<u32>() else { return axum::http::StatusCode::BAD_REQUEST.into_response() };
|
||||
let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state).await
|
||||
else { return HttpResponse::BadRequest().finish() };
|
||||
if !user.has_permissions(&app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
else { return axum::http::StatusCode::BAD_REQUEST.into_response() };
|
||||
if !user.has_permissions(&app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
if !target.has_output(&TargetType::M3u) {
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
let m3u_item = match m3u_get_item_for_stream_id(m3u_stream_id, &app_state.config, target).await {
|
||||
Ok(item) => item,
|
||||
Err(err) => {
|
||||
error!("Failed to get m3u url: {}", sanitize_sensitive_info(err.to_string().as_str()));
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let stream_url = m3u_item.get_field(resource.as_str());
|
||||
match stream_url {
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
None => axum::http::StatusCode::NOT_FOUND.into_response(),
|
||||
Some(url) => {
|
||||
if user.proxy == ProxyType::Redirect {
|
||||
debug!("Redirecting stream request to {}", sanitize_sensitive_info(&url));
|
||||
HttpResponse::Found().insert_header(("Location", url.as_str())).finish()
|
||||
redirect(url.as_str()).into_response()
|
||||
} else {
|
||||
resource_response(&app_state, url.as_str(), &req, None).await
|
||||
resource_response(&app_state, url.as_str(), &req_headers, None).await.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! register_m3u_stream_routes {
|
||||
($cfg:expr, [$($path:expr),*]) => {{
|
||||
($router:expr, [$($path:expr),*]) => {{
|
||||
$router
|
||||
$(
|
||||
$cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to(m3u_api_stream)));
|
||||
.route(&format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path), axum::routing::get(m3u_api_stream))
|
||||
// $cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to(m3u_api_stream)));
|
||||
)*
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! register_m3u_api_routes {
|
||||
($cfg:expr, [$($path:expr),*]) => {{
|
||||
($router:expr, [$($path:expr),*]) => {{
|
||||
$router
|
||||
$(
|
||||
$cfg.service(web::resource(format!("/{}", $path)).route(web::get().to(m3u_api_get)).route(web::post().to(m3u_api_post)));
|
||||
.route(&format!("/{}", $path), axum::routing::get(m3u_api_get))
|
||||
.route(&format!("/{}", $path), axum::routing::post(m3u_api_post))
|
||||
// $cfg.service(web::resource(format!("/{}", $path)).route(web::get().to(m3u_api_get)).route(web::post().to(m3u_api_post)));
|
||||
)*
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn m3u_api_register(cfg: &mut web::ServiceConfig) {
|
||||
register_m3u_api_routes!(cfg, ["get.php", "apiget", "m3u"]);
|
||||
register_m3u_stream_routes!(cfg, ["live", "movie", "series"]);
|
||||
cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{{username}}/{{password}}/{{stream_id}}")).route(web::get().to(m3u_api_stream)));
|
||||
cfg.service(web::resource(format!("/{M3U_RESOURCE_PATH}/{{username}}/{{password}}/{{stream_id}}/{{resource}}")).route(web::get().to(m3u_api_resource)));
|
||||
pub fn m3u_api_register() -> axum::Router<Arc<AppState>> {
|
||||
let mut router = axum::Router::new();
|
||||
router = register_m3u_api_routes!(router, ["get.php", "apiget", "m3u"]);
|
||||
register_m3u_stream_routes!(router, ["live", "movie", "series"])
|
||||
.route(&format!("/{M3U_STREAM_PATH}/{{username}}/{{password}}/{{stream_id}}"), axum::routing::get(m3u_api_stream))
|
||||
.route(&format!("/{M3U_RESOURCE_PATH}/{{username}}/{{password}}/{{stream_id}}/{{resource}}"), axum::routing::get(m3u_api_resource))
|
||||
}
|
||||
@@ -6,18 +6,14 @@ use crate::model::playlist::XtreamCluster;
|
||||
use crate::model::playlist_categories::PlaylistBouquetDto;
|
||||
use crate::model::xtream::PlaylistXtreamCategory;
|
||||
use crate::repository::user_repository::{load_user_bouquet_as_json, save_user_bouquet};
|
||||
use crate::repository::{m3u_repository};
|
||||
use actix_web::body::BodyStream;
|
||||
use actix_web::middleware::Compress;
|
||||
use actix_web::{web, HttpResponse};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use crate::repository::xtream_repository::xtream_get_playlist_categories;
|
||||
use crate::repository::m3u_repository;
|
||||
use bytes::Bytes;
|
||||
use futures::{stream, StreamExt};
|
||||
use log::error;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use crate::repository::xtream_repository::xtream_get_playlist_categories;
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
fn get_categories_from_xtream(categories: Option<Vec<PlaylistXtreamCategory>>) -> Vec<String> {
|
||||
let mut groups: Vec<String> = Vec::new();
|
||||
@@ -30,9 +26,9 @@ fn get_categories_from_xtream(categories: Option<Vec<PlaylistXtreamCategory>>) -
|
||||
}
|
||||
|
||||
|
||||
fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &Arc<Config>) -> Vec<String> {
|
||||
async fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &Arc<Config>) -> Vec<String> {
|
||||
let mut groups = Vec::new();
|
||||
if let Some((_guard, iter)) = m3u_repository::iter_raw_m3u_playlist(config, target) {
|
||||
if let Some((_guard, iter)) = m3u_repository::iter_raw_m3u_playlist(config, target).await {
|
||||
let mut unique_groups = HashSet::new();
|
||||
for (item, _has_next) in iter {
|
||||
if !unique_groups.contains(item.group.as_str()) {
|
||||
@@ -44,14 +40,15 @@ fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &Arc<Config>)
|
||||
groups
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn playlist_categories(
|
||||
credentials: Option<BearerAuth>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
if let Some(username) = get_username_from_auth_header(credentials, &app_state) {
|
||||
axum_auth::AuthBearer(token): axum_auth::AuthBearer,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
if let Some(username) = get_username_from_auth_header(&token, &app_state) {
|
||||
if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state).await {
|
||||
if !user.has_permissions(&app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(&app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let config = &app_state.config;
|
||||
let target_name = &target.name;
|
||||
@@ -73,7 +70,7 @@ async fn playlist_categories(
|
||||
};
|
||||
|
||||
let m3u_stream = if target.has_output(&TargetType::M3u) {
|
||||
let live_categories = get_categories_from_m3u_playlist(target, config);
|
||||
let live_categories = get_categories_from_m3u_playlist(target, config).await;
|
||||
stream::iter(vec![
|
||||
Ok::<Bytes, String>(Bytes::from(r#"{"live": "#)),
|
||||
Ok::<Bytes, String>(Bytes::from(serde_json::to_string(&live_categories).unwrap_or("[]".to_string()))),
|
||||
@@ -90,27 +87,31 @@ async fn playlist_categories(
|
||||
.chain(stream::once(async { Ok::<Bytes, String>(Bytes::from("}")) }));
|
||||
|
||||
|
||||
return HttpResponse::Ok()
|
||||
.content_type(mime::APPLICATION_JSON)
|
||||
.body(BodyStream::new(json_stream));
|
||||
return 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();
|
||||
}
|
||||
}
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn save_playlist_bouquet(
|
||||
credentials: Option<BearerAuth>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
req: web::Json<PlaylistBouquetDto>,
|
||||
) -> HttpResponse {
|
||||
if let Some(username) = get_username_from_auth_header(credentials, &app_state) {
|
||||
axum_auth::AuthBearer(token): axum_auth::AuthBearer,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(bouquet): axum::extract::Json<PlaylistBouquetDto>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
if let Some(username) = get_username_from_auth_header(&token, &app_state) {
|
||||
if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state).await {
|
||||
if !user.has_permissions(&app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(&app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match save_user_bouquet(&app_state.config, &target.name, &username, &req.0).await {
|
||||
match save_user_bouquet(&app_state.config, &target.name, &username, &bouquet).await {
|
||||
Ok(()) => {
|
||||
return HttpResponse::Ok().finish();
|
||||
return axum::http::StatusCode::OK.into_response();
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Saving bouquet for {username} failed: {err}");
|
||||
@@ -118,33 +119,53 @@ async fn save_playlist_bouquet(
|
||||
}
|
||||
}
|
||||
}
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn playlist_bouquet(
|
||||
credentials: Option<BearerAuth>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
if let Some(username) = get_username_from_auth_header(credentials, &app_state) {
|
||||
axum_auth::AuthBearer(token): axum_auth::AuthBearer,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
if let Some(username) = get_username_from_auth_header(&token, &app_state) {
|
||||
if let Some((user, _target)) = get_user_target_by_username(username.as_str(), &app_state).await {
|
||||
if !user.has_permissions(&app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(&app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let xtream = load_user_bouquet_as_json(&app_state.config, &username, TargetType::Xtream).await;
|
||||
let m3u = load_user_bouquet_as_json(&app_state.config, &username, TargetType::M3u).await;
|
||||
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(
|
||||
format!(r#"{{"xtream": {}, "m3u": {} }}"#, xtream.unwrap_or("null".to_string()), m3u.unwrap_or("null".to_string())));
|
||||
return 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();
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().content_type(mime::APPLICATION_JSON).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()
|
||||
}
|
||||
|
||||
pub fn user_api_register(app_state: Arc<AppState>) -> axum::Router<Arc<AppState>> {
|
||||
axum::Router::new()
|
||||
.nest(
|
||||
"/api/v1/user",
|
||||
axum::Router::new()
|
||||
.route("/playlist/categories", axum::routing::get(playlist_categories))
|
||||
.route("/playlist/bouquet", axum::routing::get(playlist_bouquet))
|
||||
.route("/playlist/bouquet", axum::routing::post(save_playlist_bouquet))
|
||||
.route_layer(axum::middleware::from_fn_with_state(app_state, validator_user))
|
||||
)
|
||||
|
||||
pub fn user_api_register(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::scope("/api/v1/user")
|
||||
.wrap(HttpAuthentication::with_fn(validator_user))
|
||||
.wrap(Compress::default())
|
||||
.route("/playlist/categories", web::get().to(playlist_categories))
|
||||
.route("/playlist/bouquet", web::get().to(playlist_bouquet))
|
||||
.route("/playlist/bouquet", web::post().to(save_playlist_bouquet)));
|
||||
|
||||
// cfg.service(web::scope("/api/v1/user")
|
||||
// .wrap(HttpAuthentication::with_fn(validator_user))
|
||||
// .wrap(Compress::default())
|
||||
// .route("/playlist/categories", web::get().to(playlist_categories))
|
||||
// .route("/playlist/bouquet", web::get().to(playlist_bouquet))
|
||||
// .route("/playlist/bouquet", web::post().to(save_playlist_bouquet)));
|
||||
}
|
||||
|
||||
+83
-84
@@ -1,11 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::body::BodyStream;
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::{web, HttpResponse};
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use axum::response::IntoResponse;
|
||||
use bytes::Bytes;
|
||||
use futures::{stream, StreamExt};
|
||||
use log::error;
|
||||
@@ -16,7 +12,7 @@ use crate::api::endpoints::{download_api};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::config::{ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig};
|
||||
use crate::api::model::request::{PlaylistRequest, PlaylistRequestType};
|
||||
use crate::auth::authenticator::validator_admin;
|
||||
use crate::auth::authenticator::{validator_admin};
|
||||
use crate::m3u_filter_error::M3uFilterError;
|
||||
use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, TargetUser};
|
||||
use crate::model::config::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType, TargetType};
|
||||
@@ -53,100 +49,99 @@ fn intern_save_config_main(file_path: &str, backup_dir: &str, cfg: &ConfigDto) -
|
||||
}
|
||||
|
||||
async fn save_config_api_proxy_user(
|
||||
req: web::Json<Vec<TargetUser>>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let mut users = req.0;
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(mut users): axum::extract::Json<Vec<TargetUser>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let mut usernames = HashSet::new();
|
||||
let mut tokens = HashSet::new();
|
||||
for target_user in &mut users {
|
||||
for credential in &mut target_user.credentials {
|
||||
credential.trim();
|
||||
if let Err(err) = credential.validate() {
|
||||
return HttpResponse::BadRequest().json(json!({"error": err.to_string()}));
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
if usernames.contains(&credential.username) {
|
||||
return HttpResponse::BadRequest().json(json!({"error": format!("Duplicate username {}", &credential.username)}));
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate username {}", &credential.username)}))).into_response();
|
||||
}
|
||||
usernames.insert(&credential.username);
|
||||
if let Some(token) = &credential.token {
|
||||
if tokens.contains(token) {
|
||||
return HttpResponse::BadRequest().json(json!({"error": format!("Duplicate token {token}")}));
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate token {token}")}))).into_response();
|
||||
}
|
||||
tokens.insert(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(api_proxy) = app_state.config.t_api_proxy.write().as_mut() {
|
||||
let mut lock = app_state.config.t_api_proxy.write().await;
|
||||
if let Some(api_proxy) = lock.as_mut() {
|
||||
api_proxy.user = users;
|
||||
api_proxy.user.iter_mut().flat_map(|t| &mut t.credentials).for_each(|c| c.prepare(true));
|
||||
if api_proxy.use_user_db {
|
||||
if let Err(err) = store_api_user(&app_state.config, &api_proxy.user) {
|
||||
return HttpResponse::InternalServerError().json(json!({"error": err.to_string()}));
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
} else {
|
||||
let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str();
|
||||
if let Some(err) = intern_save_config_api_proxy(backup_dir, api_proxy, app_state.config.t_api_proxy_file_path.as_str()) {
|
||||
return HttpResponse::InternalServerError().json(json!({"error": err.to_string()}));
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().finish()
|
||||
axum::http::StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
async fn save_config_main(
|
||||
req: web::Json<ConfigDto>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let cfg = req.0;
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(cfg): axum::extract::Json<ConfigDto>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
if cfg.is_valid() {
|
||||
let file_path = app_state.config.t_config_file_path.as_str();
|
||||
let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str();
|
||||
if let Some(err) = intern_save_config_main(file_path, backup_dir, &cfg) {
|
||||
return HttpResponse::InternalServerError().json(json!({"error": err.to_string()}));
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
HttpResponse::Ok().finish()
|
||||
axum::http::StatusCode::OK.into_response()
|
||||
} else {
|
||||
HttpResponse::BadRequest().json(json!({"error": "Invalid content"}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid content"}))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_config_api_proxy_config(
|
||||
req: web::Json<Vec<ApiProxyServerInfo>>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let mut req_api_proxy = req.0;
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(mut req_api_proxy): axum::extract::Json<Vec<ApiProxyServerInfo>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
for server_info in &mut req_api_proxy {
|
||||
if !server_info.is_valid() {
|
||||
return HttpResponse::BadRequest().json(json!({"error": "Invalid content"}));
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid content"}))).into_response();
|
||||
}
|
||||
}
|
||||
if let Some(api_proxy) = app_state.config.t_api_proxy.write().as_mut() {
|
||||
let mut lock = app_state.config.t_api_proxy.write().await;
|
||||
if let Some(api_proxy) = lock.as_mut() {
|
||||
api_proxy.server = req_api_proxy;
|
||||
let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str();
|
||||
if let Some(err) = intern_save_config_api_proxy(backup_dir, api_proxy, app_state.config.t_api_proxy_file_path.as_str()) {
|
||||
return HttpResponse::InternalServerError().json(json!({"error": err.to_string()}));
|
||||
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response();
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().finish()
|
||||
axum::http::StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn playlist_update(
|
||||
req: web::Json<Vec<String>>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let targets = req.0;
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(targets): axum::extract::Json<Vec<String>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let user_targets = if targets.is_empty() { None } else { Some(targets) };
|
||||
let process_targets = validate_targets(user_targets.as_ref(), &app_state.config.sources);
|
||||
match process_targets {
|
||||
Ok(valid_targets) => {
|
||||
actix_rt::spawn(playlist::exec_processing(Arc::clone(&app_state.http_client), Arc::clone(&app_state.config), Arc::new(valid_targets)));
|
||||
HttpResponse::Ok().finish()
|
||||
tokio::spawn(playlist::exec_processing(Arc::clone(&app_state.http_client), Arc::clone(&app_state.config), Arc::new(valid_targets)));
|
||||
axum::http::StatusCode::OK.into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed playlist update {}", sanitize_sensitive_info(err.to_string().as_str()));
|
||||
HttpResponse::BadRequest().json(json!({"error": err.to_string()}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": err.to_string()}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +184,7 @@ fn create_config_input_for_xtream(username: &str, password: &str, host: &str) ->
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_playlist(client: Arc<reqwest::Client>, cfg_input: Option<&ConfigInput>, cfg: &Config) -> HttpResponse {
|
||||
async fn get_playlist(client: Arc<reqwest::Client>, cfg_input: Option<&ConfigInput>, cfg: &Config) -> impl axum::response::IntoResponse + Send {
|
||||
match cfg_input {
|
||||
Some(input) => {
|
||||
let (result, errors) =
|
||||
@@ -199,12 +194,12 @@ async fn get_playlist(client: Arc<reqwest::Client>, cfg_input: Option<&ConfigInp
|
||||
};
|
||||
if result.is_empty() {
|
||||
let error_strings: Vec<String> = errors.iter().map(std::string::ToString::to_string).collect();
|
||||
HttpResponse::BadRequest().json(json!({"error": error_strings.join(", ")}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": error_strings.join(", ")}))).into_response()
|
||||
} else {
|
||||
HttpResponse::Ok().json(result)
|
||||
(axum::http::StatusCode::OK, axum::Json(result)).into_response()
|
||||
}
|
||||
}
|
||||
None => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})),
|
||||
None => (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +217,7 @@ async fn get_categories_content(action: Result<(Option<PathBuf>, Option<String>)
|
||||
None
|
||||
}
|
||||
|
||||
async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc<Config>) -> HttpResponse {
|
||||
async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc<Config>) -> impl axum::response::IntoResponse + Send {
|
||||
if let Some(target) = cfg_target {
|
||||
let target_name = &target.name;
|
||||
if target.has_output(&TargetType::Xtream) {
|
||||
@@ -230,9 +225,9 @@ async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc<Co
|
||||
let vod_categories = get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_VOD)).await;
|
||||
let series_categories = get_categories_content(xtream_repository::xtream_get_collection_path(cfg, target_name, xtream_repository::COL_CAT_SERIES)).await;
|
||||
|
||||
let live_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Live);
|
||||
let vod_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Video);
|
||||
let series_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Series);
|
||||
let live_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Live).await;
|
||||
let vod_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Video).await;
|
||||
let series_channels = xtream_repository::iter_raw_xtream_playlist(cfg, target, XtreamCluster::Series).await;
|
||||
|
||||
let live_stream = playlist_iter_to_stream(live_channels);
|
||||
let vod_stream = playlist_iter_to_stream(vod_channels);
|
||||
@@ -254,55 +249,56 @@ async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc<Co
|
||||
])).chain(series_stream).chain(stream::iter(vec![
|
||||
Ok::<Bytes, String>(Bytes::from(r"]}}")),
|
||||
]));
|
||||
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(BodyStream::new(json_stream));
|
||||
return (axum::http::StatusCode::OK, axum::body::Body::from_stream(json_stream)).into_response();
|
||||
} else if target.has_output(&TargetType::M3u) {
|
||||
return HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"}));
|
||||
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response();
|
||||
}
|
||||
}
|
||||
HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response()
|
||||
}
|
||||
|
||||
async fn playlist(
|
||||
req: web::Json<PlaylistRequest>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
match req.rtype {
|
||||
#[axum::debug_handler]
|
||||
async fn playlist_content(
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(playlist_req): axum::extract::Json<PlaylistRequest>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
match playlist_req.rtype {
|
||||
PlaylistRequestType::Input => {
|
||||
if let Some(source_id) = req.source_id {
|
||||
get_playlist(Arc::clone(&app_state.http_client), app_state.config.get_input_by_id(source_id), &app_state.config).await
|
||||
if let Some(source_id) = playlist_req.source_id {
|
||||
get_playlist(Arc::clone(&app_state.http_client), app_state.config.get_input_by_id(source_id), &app_state.config).await.into_response()
|
||||
} else {
|
||||
HttpResponse::BadRequest().json(json!({"error": "Invalid input"}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid input"}))).into_response()
|
||||
}
|
||||
}
|
||||
PlaylistRequestType::Target => {
|
||||
if let Some(source_id) = req.source_id {
|
||||
get_playlist_for_target(app_state.config.get_target_by_id(source_id), &app_state.config).await
|
||||
if let Some(source_id) = playlist_req.source_id {
|
||||
get_playlist_for_target(app_state.config.get_target_by_id(source_id), &app_state.config).await.into_response()
|
||||
} else {
|
||||
HttpResponse::BadRequest().json(json!({"error": "Invalid target"}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid target"}))).into_response()
|
||||
}
|
||||
}
|
||||
PlaylistRequestType::Xtream => {
|
||||
if let (Some(url), Some(username), Some(password)) = (req.url.as_ref(), req.username.as_ref(), req.password.as_ref()) {
|
||||
if let (Some(url), Some(username), Some(password)) = (playlist_req.url.as_ref(), playlist_req.username.as_ref(), playlist_req.password.as_ref()) {
|
||||
let input = create_config_input_for_xtream(username, password, url);
|
||||
get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await
|
||||
get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await.into_response()
|
||||
} else {
|
||||
HttpResponse::BadRequest().json(json!({"error": "Invalid url"}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid url"}))).into_response()
|
||||
}
|
||||
}
|
||||
PlaylistRequestType::M3U => {
|
||||
if let Some(url) = req.url.as_ref() {
|
||||
if let Some(url) = playlist_req.url.as_ref() {
|
||||
let input = create_config_input_for_m3u(url);
|
||||
get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await
|
||||
get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await.into_response()
|
||||
} else {
|
||||
HttpResponse::BadRequest().json(json!({"error": "Invalid url"}))
|
||||
(axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid url"}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn config(
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let map_input = |i: &ConfigInput| ServerInputConfig {
|
||||
id: i.id,
|
||||
name: i.name.clone(),
|
||||
@@ -363,24 +359,27 @@ async fn config(
|
||||
|
||||
// if we didn't read it from file then we should use it from app_state
|
||||
if result.api_proxy.is_none() {
|
||||
result.api_proxy.clone_from(&*app_state.config.t_api_proxy.read());
|
||||
result.api_proxy.clone_from(&*app_state.config.t_api_proxy.read().await);
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(result)
|
||||
axum::response::Json(result).into_response()
|
||||
}
|
||||
|
||||
pub fn v1_api_register(web_auth_enabled: bool) -> impl Fn(&mut web::ServiceConfig) {
|
||||
move |cfg: &mut web::ServiceConfig| {
|
||||
user_api_register(cfg);
|
||||
cfg.service(web::scope("/api/v1")
|
||||
.wrap(Condition::new(web_auth_enabled, HttpAuthentication::with_fn(validator_admin)))
|
||||
.route("/config", web::get().to(config))
|
||||
.route("/config/main", web::post().to(save_config_main))
|
||||
.route("/config/user", web::post().to(save_config_api_proxy_user))
|
||||
.route("/config/apiproxy", web::post().to(save_config_api_proxy_config))
|
||||
.route("/playlist", web::post().to(playlist))
|
||||
.route("/playlist/update", web::post().to(playlist_update))
|
||||
.route("/file/download", web::post().to(download_api::queue_download_file))
|
||||
.route("/file/download/info", web::get().to(download_api::download_file_info)));
|
||||
pub fn v1_api_register(web_auth_enabled: bool, app_state: Arc<AppState>) -> axum::Router<Arc<AppState>> {
|
||||
let mut router = axum::Router::new();
|
||||
router = router.route("/config", axum::routing::get(config))
|
||||
.route("/config/main", axum::routing::post(save_config_main))
|
||||
.route("/config/user", axum::routing::post(save_config_api_proxy_user))
|
||||
.route("/config/apiproxy", axum::routing::post(save_config_api_proxy_config))
|
||||
.route("/playlist", axum::routing::post(playlist_content))
|
||||
.route("/playlist/update", axum::routing::post(playlist_update))
|
||||
.route("/file/download", axum::routing::post(download_api::queue_download_file))
|
||||
.route("/file/download/info", axum::routing::get(download_api::download_file_info));
|
||||
if web_auth_enabled {
|
||||
router = router.route_layer(axum::middleware::from_fn_with_state(Arc::clone(&app_state), validator_admin));
|
||||
}
|
||||
|
||||
axum::Router::new()
|
||||
.merge(user_api_register(app_state))
|
||||
.nest("/api/v1", router)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use axum::response::IntoResponse;
|
||||
use crate::api::api_utils::serve_file;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::auth::authenticator::{create_jwt_admin, create_jwt_user, is_admin, verify_token};
|
||||
use crate::auth::password::verify_password;
|
||||
use crate::auth::user::UserCredential;
|
||||
|
||||
fn no_web_auth_token() -> HttpResponse {
|
||||
HttpResponse::Ok().json(HashMap::from([("token", "authorized")]))
|
||||
fn no_web_auth_token() -> impl axum::response::IntoResponse + Send {
|
||||
axum::Json(HashMap::from([("token", "authorized")])).into_response()
|
||||
}
|
||||
|
||||
async fn token(
|
||||
mut req: web::Json<UserCredential>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(mut req): axum::extract::Json<UserCredential>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
match &app_state.config.web_auth {
|
||||
None => no_web_auth_token(),
|
||||
None => no_web_auth_token().into_response(),
|
||||
Some(web_auth) => {
|
||||
if !web_auth.enabled {
|
||||
return no_web_auth_token();
|
||||
return no_web_auth_token().into_response();
|
||||
}
|
||||
let username = req.username.as_str();
|
||||
let password = req.password.as_str();
|
||||
@@ -31,41 +30,38 @@ async fn token(
|
||||
if verify_password(hash, password.as_bytes()) {
|
||||
if let Ok(token) = create_jwt_admin(web_auth, username) {
|
||||
req.zeroize();
|
||||
return HttpResponse::Ok().json(HashMap::from([("token", token)]));
|
||||
return axum::Json(HashMap::from([("token", token)])).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(credentials) = app_state.config.get_user_credentials(username) {
|
||||
if let Some(credentials) = app_state.config.get_user_credentials(username).await {
|
||||
if credentials.password == password {
|
||||
if let Ok(token) = create_jwt_user(web_auth, username) {
|
||||
req.zeroize();
|
||||
return HttpResponse::Ok().json(HashMap::from([("token", token)]));
|
||||
return axum::Json(HashMap::from([("token", token)])).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.zeroize();
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn token_refresh(
|
||||
_req: HttpRequest,
|
||||
credentials: Option<BearerAuth>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
axum_auth::AuthBearer(token): axum_auth::AuthBearer,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
match &app_state.config.web_auth {
|
||||
None => {
|
||||
no_web_auth_token()
|
||||
},
|
||||
None => no_web_auth_token().into_response(),
|
||||
Some(web_auth) => {
|
||||
if !web_auth.enabled {
|
||||
return no_web_auth_token();
|
||||
return no_web_auth_token().into_response();
|
||||
}
|
||||
let secret_key = web_auth.secret.as_ref();
|
||||
let maybe_token_data = verify_token(credentials, secret_key);
|
||||
let maybe_token_data = verify_token(&token, secret_key);
|
||||
if let Some(token_data) = maybe_token_data {
|
||||
let username = token_data.claims.username.clone();
|
||||
let web_auth_cfg = app_state.config.web_auth.as_ref().unwrap();
|
||||
@@ -75,29 +71,37 @@ async fn token_refresh(
|
||||
create_jwt_user(web_auth_cfg, &username)
|
||||
};
|
||||
if let Ok(token) = new_token {
|
||||
return HttpResponse::Ok().json(HashMap::from([("token", token)]));
|
||||
return axum::Json(HashMap::from([("token", token)])).into_response();
|
||||
}
|
||||
}
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn index(
|
||||
_req: HttpRequest,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> std::io::Result<NamedFile> {
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let path: PathBuf = [&app_state.config.api.web_root, "index.html"].iter().collect();
|
||||
NamedFile::open(path)
|
||||
serve_file(&path, mime::TEXT_HTML_UTF_8).await.into_response()
|
||||
}
|
||||
|
||||
pub fn index_register(web_dir_path: &Path) -> impl Fn(&mut web::ServiceConfig) + '_ {
|
||||
move |cfg: &mut web::ServiceConfig| {
|
||||
cfg.service(web::scope("/auth")
|
||||
.route("/token", web::post().to(token))
|
||||
.route("/refresh", web::post().to(token_refresh)));
|
||||
cfg.service(web::scope("")
|
||||
.route("/", web::get().to(index))
|
||||
.service(actix_files::Files::new("", web_dir_path)));
|
||||
}
|
||||
}
|
||||
pub fn index_register(web_dir_path: &Path) -> axum::Router<Arc<AppState>> {
|
||||
axum::Router::new()
|
||||
.nest("/auth", axum::Router::new()
|
||||
.route("/token", axum::routing::post(token))
|
||||
.route("/refresh", axum::routing::post(token_refresh)))
|
||||
.merge(axum::Router::new()
|
||||
.route("/", axum::routing::get(index))
|
||||
.fallback(axum::routing::get_service(tower_http::services::ServeDir::new(web_dir_path))))
|
||||
}
|
||||
// pub fn index_register(web_dir_path: &Path) -> impl Fn(&mut web::ServiceConfig) + '_ {
|
||||
// move |cfg: &mut web::ServiceConfig| {
|
||||
// cfg.service(web::scope("/auth")
|
||||
// .route("/token", web::post().to(token))
|
||||
// .route("/refresh", web::post().to(token_refresh)));
|
||||
// cfg.service(web::scope("")
|
||||
// .route("/", web::get().to(index))
|
||||
// .service(actix_files::Files::new("", web_dir_path)));
|
||||
// }
|
||||
// }
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use actix_web::{http::header, web, HttpRequest, HttpResponse};
|
||||
use log::{error, trace};
|
||||
use quick_xml::{Reader, Writer};
|
||||
use std::sync::Arc;
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{Duration, NaiveDateTime, TimeDelta};
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
// use actix_web::{http::header, web, HttpRequest, HttpResponse};
|
||||
use log::{error, trace};
|
||||
use quick_xml::events::{BytesStart, Event};
|
||||
use chrono::{Duration, NaiveDateTime, TimeDelta};
|
||||
use quick_xml::{Reader, Writer};
|
||||
|
||||
use crate::api::api_utils::{get_user_target, serve_file};
|
||||
use crate::api::model::app_state::AppState;
|
||||
@@ -21,9 +21,12 @@ use crate::repository::xtream_repository::{xtream_get_epg_file_path, xtream_get_
|
||||
use crate::utils::file::file_utils;
|
||||
use crate::utils::file::file_utils::file_reader;
|
||||
|
||||
pub fn get_empty_epg_response() -> HttpResponse {
|
||||
HttpResponse::Ok().content_type(mime::TEXT_XML).body(
|
||||
r#"<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE tv SYSTEM "xmltv.dtd"><tv generator-info-name="Xtream Codes" generator-info-url=""></tv>"#)
|
||||
pub fn get_empty_epg_response() -> impl axum::response::IntoResponse + Send {
|
||||
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()
|
||||
}
|
||||
|
||||
fn time_correct(date_time: &str, correction: &TimeDelta) -> String {
|
||||
@@ -35,12 +38,12 @@ fn time_correct(date_time: &str, correction: &TimeDelta) -> String {
|
||||
|
||||
// Parse the datetime string
|
||||
NaiveDateTime::parse_from_str(date_time_split[0], "%Y%m%d%H%M%S").map_or_else(|_| date_time.to_string(), |native_dt| {
|
||||
let corrected_dt = native_dt + *correction;
|
||||
// Format the corrected datetime back to string
|
||||
let formatted_dt = corrected_dt.format("%Y%m%d%H%M%S").to_string();
|
||||
let result = format!("{} {}", formatted_dt, date_time_split[1]);
|
||||
result
|
||||
})
|
||||
let corrected_dt = native_dt + *correction;
|
||||
// Format the corrected datetime back to string
|
||||
let formatted_dt = corrected_dt.format("%Y%m%d%H%M%S").to_string();
|
||||
let result = format!("{} {}", formatted_dt, date_time_split[1]);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn get_epg_path_for_target_of_type(target_name: &str, epg_path: PathBuf) -> Option<PathBuf> {
|
||||
@@ -89,23 +92,23 @@ fn parse_timeshift(time_shift: Option<&String>) -> Option<i32> {
|
||||
})
|
||||
}
|
||||
|
||||
async fn serve_epg(epg_path: &Path, req: &HttpRequest, user: &ProxyUserCredentials) -> HttpResponse {
|
||||
async fn serve_epg(epg_path: &Path, user: &ProxyUserCredentials) -> impl axum::response::IntoResponse + Send {
|
||||
match File::open(epg_path) {
|
||||
Ok(epg_file) => {
|
||||
match parse_timeshift(user.epg_timeshift.as_ref()) {
|
||||
None => serve_file(epg_path, req, mime::TEXT_XML).await,
|
||||
None => serve_file(epg_path, mime::TEXT_XML).await.into_response(),
|
||||
Some(duration) => {
|
||||
serve_epg_with_timeshift(epg_file, duration)
|
||||
serve_epg_with_timeshift(epg_file, duration).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
get_empty_epg_response()
|
||||
get_empty_epg_response().into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> HttpResponse {
|
||||
fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> impl axum::response::IntoResponse + Send {
|
||||
let reader = file_reader(epg_file);
|
||||
let encoder = GzEncoder::new(Vec::with_capacity(4096), Compression::default());
|
||||
let mut xml_reader = Reader::from_reader(reader);
|
||||
@@ -160,36 +163,38 @@ fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> HttpResponse
|
||||
}
|
||||
|
||||
let compressed_data = xml_writer.into_inner().finish().unwrap();
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/octet-stream")
|
||||
.insert_header((header::CONTENT_ENCODING, "gzip")) // Set Content-Encoding header
|
||||
.body(compressed_data)
|
||||
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()
|
||||
}
|
||||
|
||||
async fn xmltv_api(
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
req: HttpRequest,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
if let Some((user, target)) = get_user_target(&api_req, &app_state).await {
|
||||
if !user.has_permissions(&app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(&app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match get_epg_path_for_target(&app_state.config, target) {
|
||||
None => {
|
||||
// No epg configured, No processing or timeshift, epg can't be mapped to the channels.
|
||||
// we do not deliver epg
|
||||
}
|
||||
Some(epg_path) => return serve_epg(&epg_path, &req, &user).await
|
||||
Some(epg_path) => return serve_epg(&epg_path, &user).await.into_response()
|
||||
}
|
||||
}
|
||||
get_empty_epg_response()
|
||||
get_empty_epg_response().into_response()
|
||||
}
|
||||
|
||||
pub fn xmltv_api_register(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("/xmltv.php").route(web::get().to(xmltv_api)))
|
||||
.service(web::resource("/update/epg.php").route(web::get().to(xmltv_api)))
|
||||
.service(web::resource("/epg").route(web::get().to(xmltv_api)));
|
||||
pub fn xmltv_api_register() -> axum::Router<Arc<AppState>> {
|
||||
axum::Router::new()
|
||||
.route("/xmltv.php", axum::routing::get(xmltv_api))
|
||||
.route("/update/epg.php", axum::routing::get(xmltv_api))
|
||||
.route("/epg", axum::routing::get(xmltv_api))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+184
-132
@@ -1,18 +1,19 @@
|
||||
// https://github.com/tellytv/go.xtream-codes/blob/master/structs.go
|
||||
|
||||
use crate::api::api_utils::{try_option_bad_request, try_result_bad_request};
|
||||
use crate::api::api_utils::{redirect, try_option_bad_request, try_result_bad_request};
|
||||
use crate::utils::trace_if_enabled;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use futures::Stream;
|
||||
use log::{debug, error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use crate::api::api_utils;
|
||||
use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, resource_response, separate_number_and_remainder, serve_file, stream_response};
|
||||
@@ -48,7 +49,7 @@ const TAG_CATEGORY_ID: &str = "category_id";
|
||||
const TAG_STREAM_ID: &str = "stream_id";
|
||||
const TAG_EPG_LISTINGS: &str = "epg_listings";
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum XtreamApiStreamContext {
|
||||
LiveAlt,
|
||||
Live,
|
||||
@@ -75,7 +76,7 @@ impl Display for XtreamApiStreamContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct XtreamApiStreamRequest<'a> {
|
||||
context: XtreamApiStreamContext,
|
||||
username: &'a str,
|
||||
@@ -100,9 +101,9 @@ impl<'a> XtreamApiStreamRequest<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serve_query(file_path: &Path, filter: &HashMap<&str, HashSet<String>>) -> HttpResponse {
|
||||
pub fn serve_query(file_path: &Path, filter: &HashMap<&str, HashSet<String>>) -> impl axum::response::IntoResponse + Send {
|
||||
let filtered = json_utils::json_filter_file(file_path, filter);
|
||||
HttpResponse::Ok().json(filtered)
|
||||
axum::Json(filtered)
|
||||
}
|
||||
|
||||
fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &XtreamApiStreamContext, action_path: &str, fallback_url: &str) -> Option<String> {
|
||||
@@ -132,26 +133,26 @@ fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &XtreamApiStre
|
||||
}
|
||||
}
|
||||
|
||||
fn get_user_info(user: &ProxyUserCredentials, app_state: &AppState) -> XtreamAuthorizationResponse {
|
||||
let server_info = app_state.config.get_user_server_info(user);
|
||||
let active_connections = app_state.get_active_connections_for_user(&user.username);
|
||||
async fn get_user_info(user: &ProxyUserCredentials, app_state: &AppState) -> XtreamAuthorizationResponse {
|
||||
let server_info = app_state.config.get_user_server_info(user).await;
|
||||
let active_connections = app_state.get_active_connections_for_user(&user.username).await;
|
||||
XtreamAuthorizationResponse::new(&server_info, user, active_connections, app_state.config.user_access_control)
|
||||
}
|
||||
|
||||
async fn xtream_player_api_stream(
|
||||
req: &HttpRequest,
|
||||
api_req: &web::Query<UserApiRequest>,
|
||||
app_state: &web::Data<Arc<AppState>>,
|
||||
req_headers: &HeaderMap,
|
||||
api_req: &UserApiRequest,
|
||||
app_state: &Arc<AppState>,
|
||||
stream_req: XtreamApiStreamRequest<'_>,
|
||||
) -> HttpResponse {
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let (user, target) = try_option_bad_request!(get_user_target_by_credentials(stream_req.username, stream_req.password, api_req, app_state).await, false, format!("Could not find any user {}", stream_req.username));
|
||||
if !user.has_permissions(app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let target_name = &target.name;
|
||||
if !target.has_output(&TargetType::Xtream) {
|
||||
debug!("Target has no xtream output {}", target_name);
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
let (action_stream_id, stream_ext) = separate_number_and_remainder(stream_req.stream_id);
|
||||
let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse());
|
||||
@@ -160,7 +161,7 @@ async fn xtream_player_api_stream(
|
||||
|
||||
if pli.item_type == PlaylistItemType::LiveHls {
|
||||
debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(&pli.url));
|
||||
return HttpResponse::Found().insert_header(("Location", pli.url.to_string())).finish();
|
||||
return redirect(&pli.url).into_response();
|
||||
}
|
||||
|
||||
let is_hls_request = stream_ext.as_deref() == Some(HLS_EXT);
|
||||
@@ -172,17 +173,17 @@ async fn xtream_player_api_stream(
|
||||
let username = input.username.as_ref().map_or("", |v| v);
|
||||
let password = input.password.as_ref().map_or("", |v| v);
|
||||
let stream_url = format!("{url}/series/{username}/{password}/{}{ext}", mapping.provider_id);
|
||||
return HttpResponse::Found().insert_header(("Location", stream_url)).finish();
|
||||
return redirect(&stream_url).into_response();
|
||||
}
|
||||
|
||||
let redirect_url = if is_hls_request { &replace_extension(&pli.url, "m3u8") } else { &pli.url };
|
||||
debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(redirect_url));
|
||||
return HttpResponse::Found().insert_header(("Location", redirect_url.as_str())).finish();
|
||||
return redirect(redirect_url.as_str()).into_response();
|
||||
}
|
||||
|
||||
// Reverse proxy mode
|
||||
if is_hls_request {
|
||||
return handle_hls_stream_request(app_state, &user, &pli, input, TargetType::Xtream).await;
|
||||
return handle_hls_stream_request(app_state, &user, &pli, input, TargetType::Xtream).await.into_response();
|
||||
}
|
||||
|
||||
let extension = stream_ext.unwrap_or_else(
|
||||
@@ -200,7 +201,7 @@ async fn xtream_player_api_stream(
|
||||
stream_req.context));
|
||||
|
||||
trace_if_enabled!("Streaming stream request from {}", sanitize_sensitive_info(&stream_url));
|
||||
stream_response(app_state, &stream_url, req, Some(input), pli.item_type, target, &user).await
|
||||
stream_response(app_state, &stream_url, req_headers, Some(input), pli.item_type, target, &user).await.into_response()
|
||||
}
|
||||
|
||||
|
||||
@@ -215,18 +216,18 @@ fn get_doc_id_and_field_name(input: &str) -> Option<(u32, &str)> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_doc_resource_field_value(field: &str, doc: Option<&Value>) -> Option<Rc<String>> {
|
||||
fn get_doc_resource_field_value(field: &str, doc: Option<&Value>) -> Option<String> {
|
||||
if let Some(Value::Object(info_data)) = doc {
|
||||
if field.starts_with(PROP_BACKDROP_PATH) {
|
||||
return get_backdrop_path_value(field, info_data.get(PROP_BACKDROP_PATH));
|
||||
} else if let Some(Value::String(url)) = info_data.get(field) {
|
||||
return Some(Rc::new(url.to_string()));
|
||||
return Some(url.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn xtream_get_info_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result<Option<Rc<String>>, serde_json::Error> {
|
||||
fn xtream_get_info_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result<Option<String>, serde_json::Error> {
|
||||
let info_content = match pli.xtream_cluster {
|
||||
XtreamCluster::Video => {
|
||||
xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id())
|
||||
@@ -301,7 +302,7 @@ fn get_season_info_doc(doc: &Vec<Value>, season_id: u32) -> Option<&Value> {
|
||||
}
|
||||
|
||||
|
||||
fn xtream_get_season_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result<Option<Rc<String>>, serde_json::Error> {
|
||||
fn xtream_get_season_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result<Option<String>, serde_json::Error> {
|
||||
let info_content = match pli.xtream_cluster {
|
||||
XtreamCluster::Series => {
|
||||
xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id())
|
||||
@@ -330,19 +331,19 @@ fn xtream_get_season_resource_url(config: &Config, pli: &XtreamPlaylistItem, tar
|
||||
}
|
||||
|
||||
async fn xtream_player_api_resource(
|
||||
req: &HttpRequest,
|
||||
api_req: &web::Query<UserApiRequest>,
|
||||
app_state: &web::Data<Arc<AppState>>,
|
||||
req_headers: &HeaderMap,
|
||||
api_req: &UserApiRequest,
|
||||
app_state: &Arc<AppState>,
|
||||
resource_req: XtreamApiStreamRequest<'_>,
|
||||
) -> HttpResponse {
|
||||
) -> impl axum::response::IntoResponse {
|
||||
let (user, target) = try_option_bad_request!(get_user_target_by_credentials(resource_req.username, resource_req.password, api_req, app_state).await, false, format!("Could not find any user {}", resource_req.username));
|
||||
if !user.has_permissions(app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let target_name = &target.name;
|
||||
if !target.has_output(&TargetType::Xtream) {
|
||||
debug!("Target has no xtream output {}", target_name);
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
let virtual_id: u32 = try_result_bad_request!(resource_req.stream_id.trim().parse());
|
||||
let resource = resource_req.action_path.trim();
|
||||
@@ -356,43 +357,41 @@ async fn xtream_player_api_resource(
|
||||
};
|
||||
|
||||
match stream_url {
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
None => axum::http::StatusCode::NOT_FOUND.into_response(),
|
||||
Some(url) => {
|
||||
if user.proxy == ProxyType::Redirect {
|
||||
trace_if_enabled!("Redirecting resource request to {}", sanitize_sensitive_info(&url));
|
||||
HttpResponse::Found().insert_header(("Location", url.as_str())).finish()
|
||||
redirect(url.as_str()).into_response()
|
||||
} else {
|
||||
trace_if_enabled!("Resource request to {}", sanitize_sensitive_info(&url));
|
||||
resource_response(app_state, url.as_str(), req, None).await
|
||||
resource_response(app_state, url.as_str(), req_headers, None).await.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! create_xtream_player_api_stream {
|
||||
($fn_name:ident, $context:expr) => {
|
||||
async fn $fn_name(
|
||||
req: HttpRequest,
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
path: web::Path<(String, String, String)>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let (username, password, stream_id) = path.into_inner();
|
||||
xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, "")).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! create_xtream_player_api_resource {
|
||||
($fn_name:ident, $context:expr) => {
|
||||
async fn $fn_name(
|
||||
req: HttpRequest,
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
path: web::Path<(String, String, String, String)>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let (username, password, stream_id, resource) = path.into_inner();
|
||||
xtream_player_api_resource(&req, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, &resource)).await
|
||||
axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<(String, String, String, String)>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
req_headers: HeaderMap,
|
||||
) -> impl axum::response::IntoResponse {
|
||||
xtream_player_api_resource(&req_headers, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, &resource)).await.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! create_xtream_player_api_stream {
|
||||
($fn_name:ident, $context:expr) => {
|
||||
async fn $fn_name(
|
||||
axum::extract::Path((username, password, stream_id)): axum::extract::Path<(String, String, String)>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
req_headers: HeaderMap,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
xtream_player_api_stream(&req_headers, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, "")).await.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,41 +415,46 @@ fn get_non_empty<'a>(first: &'a str, second: &'a str, third: &'a str) -> &'a str
|
||||
}
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn xtream_player_api_timeshift_stream(
|
||||
req: HttpRequest,
|
||||
api_query_req: web::Query<UserApiRequest>,
|
||||
api_form_req: web::Form<UserApiRequest>,
|
||||
path: web::Path<(String, String, String, String, String)>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
let (path_username, path_password, path_duration, path_start, path_stream_id) = path.into_inner();
|
||||
req_headers: HeaderMap,
|
||||
axum::extract::Query(api_query_req): axum::extract::Query<UserApiRequest>,
|
||||
axum::extract::Path((path_username, path_password, path_duration, path_start, path_stream_id)): axum::extract::Path<(String, String, String, String, String)>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Form(api_form_req): axum::extract::Form<UserApiRequest>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let username = get_non_empty(&path_username, &api_query_req.username, &api_form_req.username);
|
||||
let password = get_non_empty(&path_password, &api_query_req.password, &api_form_req.password);
|
||||
let stream_id = get_non_empty(&path_stream_id, &api_query_req.stream, &api_form_req.stream);
|
||||
let duration = get_non_empty(&path_duration, &api_query_req.duration, &api_form_req.duration);
|
||||
let start = get_non_empty(&path_start, &api_query_req.start, &api_form_req.start);
|
||||
let action_path = format!("{duration}/{start}");
|
||||
xtream_player_api_stream(&req, &api_query_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Timeshift, username, password, stream_id, &action_path)).await
|
||||
xtream_player_api_stream(&req_headers, &api_query_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Timeshift, username, password, stream_id, &action_path)).await
|
||||
}
|
||||
|
||||
async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserCredentials,
|
||||
target: &ConfigTarget, stream_id: &str,
|
||||
cluster: XtreamCluster) -> HttpResponse {
|
||||
cluster: XtreamCluster) -> impl axum::response::IntoResponse + Send {
|
||||
let virtual_id: u32 = match FromStr::from_str(stream_id) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return HttpResponse::BadRequest().finish()
|
||||
Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
};
|
||||
|
||||
if let Ok((pli, virtual_record)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(cluster)) {
|
||||
if pli.provider_id > 0 {
|
||||
let input_name = Rc::clone(&pli.input_name);
|
||||
let input_name = &pli.input_name;
|
||||
if let Some(input) = app_state.config.get_input_by_name(input_name.as_str()) {
|
||||
if let Some(info_url) = xtream::get_xtream_player_api_info_url(input, cluster, pli.provider_id) {
|
||||
// Redirect is only possible for live streams, vod and series info needs to be modified
|
||||
if user.proxy == ProxyType::Redirect && cluster == XtreamCluster::Live {
|
||||
return HttpResponse::Found().insert_header(("Location", info_url)).finish();
|
||||
return redirect(&info_url).into_response();
|
||||
} else if let Ok(content) = xtream::get_xtream_stream_info(Arc::clone(&app_state.http_client), &app_state.config, user, input, target, &pli, info_url.as_str(), cluster).await {
|
||||
return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content);
|
||||
return axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from(content))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,25 +463,40 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC
|
||||
return match cluster {
|
||||
XtreamCluster::Video => {
|
||||
let content = create_vod_info_from_item(user, &pli, virtual_record.last_updated);
|
||||
HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content)
|
||||
axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from(content))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
XtreamCluster::Live | XtreamCluster::Series => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{}"),
|
||||
XtreamCluster::Live | XtreamCluster::Series => axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from("{}".as_bytes()))
|
||||
.unwrap()
|
||||
.into_response(),
|
||||
};
|
||||
}
|
||||
HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{}")
|
||||
axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from("{}".as_bytes()))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, limit: &str) -> HttpResponse {
|
||||
async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, limit: &str) -> impl axum::response::IntoResponse + Send {
|
||||
let target_name = &target.name;
|
||||
if target.has_output(&TargetType::Xtream) {
|
||||
let virtual_id: u32 = match FromStr::from_str(stream_id.trim()) {
|
||||
Ok(id) => id,
|
||||
Err(_) => return HttpResponse::BadRequest().finish()
|
||||
Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
};
|
||||
|
||||
if let Ok((pli, _)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None) {
|
||||
if pli.provider_id > 0 {
|
||||
let input_name = Rc::clone(&pli.input_name);
|
||||
let input_name = &pli.input_name;
|
||||
if let Some(input) = app_state.config.get_input_by_name(input_name.as_str()) {
|
||||
if let Some(action_url) = xtream::get_xtream_player_api_action_url(input, ACTION_GET_SHORT_EPG) {
|
||||
let mut info_url = format!("{action_url}&{TAG_STREAM_ID}={}", pli.provider_id);
|
||||
@@ -485,14 +504,14 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials,
|
||||
info_url = format!("{info_url}&limit={limit}");
|
||||
}
|
||||
if user.proxy == ProxyType::Redirect {
|
||||
return HttpResponse::Found().insert_header(("Location", info_url)).finish();
|
||||
return redirect(&info_url).into_response();
|
||||
}
|
||||
|
||||
return match request::download_text_content(Arc::clone(&app_state.http_client), input, info_url.as_str(), None).await {
|
||||
Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content),
|
||||
Ok(content) => (axum::http::StatusCode::OK, axum::Json(content)).into_response(),
|
||||
Err(err) => {
|
||||
error!("Failed to download epg {}", sanitize_sensitive_info(err.to_string().as_str()));
|
||||
get_empty_epg_response()
|
||||
get_empty_epg_response().into_response()
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -501,10 +520,10 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials,
|
||||
}
|
||||
}
|
||||
warn!("Cant find short epg with id: {target_name}/{stream_id}");
|
||||
get_empty_epg_response()
|
||||
get_empty_epg_response().into_response()
|
||||
}
|
||||
|
||||
async fn xtream_player_api_handle_content_action(config: &Config, target_name: &str, action: &str, category_id: Option<u32>, user: &ProxyUserCredentials, req: &HttpRequest) -> Option<HttpResponse> {
|
||||
async fn xtream_player_api_handle_content_action(config: &Config, target_name: &str, action: &str, category_id: Option<u32>, user: &ProxyUserCredentials) -> Option<impl IntoResponse> {
|
||||
if let Ok((path, content)) = match action {
|
||||
ACTION_GET_LIVE_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_LIVE),
|
||||
ACTION_GET_VOD_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_VOD),
|
||||
@@ -520,18 +539,20 @@ async fn xtream_player_api_handle_content_action(config: &Config, target_name: &
|
||||
_ => None
|
||||
};
|
||||
if let Some(flt) = filter {
|
||||
return Some(serve_query(&file_path, &HashMap::from([(TAG_CATEGORY_ID, flt)])));
|
||||
return Some(serve_query(&file_path, &HashMap::from([(TAG_CATEGORY_ID, flt)])).into_response());
|
||||
}
|
||||
return Some(serve_file(&file_path, req, mime::APPLICATION_JSON).await);
|
||||
return Some(serve_file(&file_path, mime::APPLICATION_JSON).await.into_response());
|
||||
} else if let Some(payload) = content {
|
||||
return Some(HttpResponse::Ok().body(payload));
|
||||
return Some(axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.body(payload).unwrap().into_response());
|
||||
}
|
||||
return Some(api_utils::empty_json_list_response());
|
||||
return Some(api_utils::empty_json_list_response().into_response());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget, stream_id: &str, start: &str, end: &str) -> HttpResponse {
|
||||
async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget, stream_id: &str, start: &str, end: &str) -> impl axum::response::IntoResponse + Send {
|
||||
let virtual_id: u32 = try_result_bad_request!(FromStr::from_str(stream_id));
|
||||
let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(XtreamCluster::Live)));
|
||||
let input = try_option_bad_request!(app_state.config.get_input_by_name(pli.input_name.as_str()));
|
||||
@@ -540,29 +561,35 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget
|
||||
let mut doc: Map<String, Value> = try_result_bad_request!(serde_json::from_str(&content));
|
||||
let epg_listings = try_option_bad_request!(doc.get_mut(TAG_EPG_LISTINGS).and_then(Value::as_array_mut));
|
||||
let target_path = try_option_bad_request!(get_target_storage_path(&app_state.config, target.name.as_str()));
|
||||
let (mut target_id_mapping, file_lock) = get_target_id_mapping(&app_state.config, &target_path);
|
||||
let (mut target_id_mapping, file_lock) = get_target_id_mapping(&app_state.config, &target_path).await;
|
||||
for epg_list_item in epg_listings.iter_mut().filter_map(Value::as_object_mut) {
|
||||
// TODO epg_id
|
||||
if let Some(catchup_provider_id) = epg_list_item.get(TAG_ID).and_then(Value::as_str).and_then(|id| id.parse::<u32>().ok()) {
|
||||
let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &catchup_provider_id.to_string(), pli.item_type, &pli.url);
|
||||
let virtual_id = target_id_mapping.get_and_update_virtual_id(uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id);
|
||||
let virtual_id = target_id_mapping.get_and_update_virtual_id(&uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id);
|
||||
epg_list_item.insert(TAG_ID.to_string(), Value::String(virtual_id.to_string()));
|
||||
}
|
||||
}
|
||||
if let Err(err) = target_id_mapping.persist() {
|
||||
error!("Failed to write catchup id mapping {err}");
|
||||
return HttpResponse::BadRequest().finish();
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
drop(file_lock);
|
||||
serde_json::to_string(&doc).map_or_else(|_| HttpResponse::BadRequest().finish(), |result| HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(result))
|
||||
serde_json::to_string(&doc)
|
||||
.map_or_else(
|
||||
|_| axum::http::StatusCode::BAD_REQUEST.into_response(),
|
||||
|result| axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string())
|
||||
.body(result).unwrap().into_response())
|
||||
}
|
||||
|
||||
macro_rules! skip_json_response_if_flag_set {
|
||||
($flag:expr, $stmt:expr) => {
|
||||
if $flag {
|
||||
return api_utils::empty_json_list_response();
|
||||
return api_utils::empty_json_list_response().into_response();
|
||||
}
|
||||
return $stmt;
|
||||
return $stmt.into_response();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -577,23 +604,22 @@ macro_rules! skip_flag_optional {
|
||||
}
|
||||
|
||||
async fn xtream_player_api(
|
||||
req: &HttpRequest,
|
||||
api_req: UserApiRequest,
|
||||
app_state: &web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
app_state: &Arc<AppState>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let user_target = get_user_target(&api_req, app_state).await;
|
||||
if let Some((user, target)) = user_target {
|
||||
if !target.has_output(&TargetType::Xtream) {
|
||||
return HttpResponse::Ok().json(get_user_info(&user, app_state));
|
||||
return axum::response::Json(get_user_info(&user, app_state).await).into_response();
|
||||
}
|
||||
|
||||
let action = api_req.action.trim();
|
||||
if action.is_empty() {
|
||||
return HttpResponse::Ok().json(get_user_info(&user, app_state));
|
||||
return axum::response::Json(get_user_info(&user, app_state).await).into_response();
|
||||
}
|
||||
|
||||
if !user.has_permissions(app_state) {
|
||||
return HttpResponse::Forbidden().finish();
|
||||
if !user.has_permissions(app_state).await {
|
||||
return axum::http::StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
|
||||
// Process specific playlist actions
|
||||
@@ -616,7 +642,7 @@ async fn xtream_player_api(
|
||||
ACTION_GET_EPG | ACTION_GET_SHORT_EPG => {
|
||||
return xtream_get_short_epg(
|
||||
app_state, &user, target, api_req.stream_id.trim(), api_req.limit.trim(),
|
||||
).await;
|
||||
).await.into_response();
|
||||
}
|
||||
ACTION_GET_CATCHUP_TABLE => {
|
||||
skip_json_response_if_flag_set!(skip_live, xtream_get_catchup_response(app_state, target, api_req.stream_id.trim(), api_req.start.trim(), api_req.end.trim()).await);
|
||||
@@ -627,9 +653,9 @@ async fn xtream_player_api(
|
||||
let category_id = api_req.category_id.trim().parse::<u32>().ok();
|
||||
// Handle general content actions
|
||||
if let Some(response) = xtream_player_api_handle_content_action(
|
||||
&app_state.config, &target.name, action, category_id, &user, req,
|
||||
&app_state.config, &target.name, action, category_id, &user,
|
||||
).await {
|
||||
return response;
|
||||
return response.into_response();
|
||||
}
|
||||
|
||||
let result = match action {
|
||||
@@ -649,20 +675,21 @@ 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);
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime::APPLICATION_JSON)
|
||||
.streaming(content_stream)
|
||||
axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::OK)
|
||||
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string())
|
||||
.body(axum::body::Body::from_stream(content_stream)).unwrap().into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed response for xtream target: {} action: {} error: {}", &target.name, action, err);
|
||||
// Some players fail on NoContent, so we return an empty array
|
||||
api_utils::empty_json_list_response()
|
||||
api_utils::empty_json_list_response().into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Some players fail on NoContent, so we return an empty array
|
||||
api_utils::empty_json_list_response()
|
||||
api_utils::empty_json_list_response().into_response()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -671,7 +698,7 @@ async fn xtream_player_api(
|
||||
(_, true) => debug!("Parameter action is empty!"),
|
||||
_ => debug!("Bad request!"),
|
||||
}
|
||||
HttpResponse::BadRequest().finish()
|
||||
axum::http::StatusCode::BAD_REQUEST.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,65 +713,90 @@ fn xtream_create_content_stream(xtream_iter: impl Iterator<Item=(String, bool)>)
|
||||
})).chain(stream::once(async { Ok::<Bytes, String>(Bytes::from("]")) })))
|
||||
}
|
||||
|
||||
async fn xtream_player_api_get(req: HttpRequest,
|
||||
api_req: web::Query<UserApiRequest>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
xtream_player_api(&req, api_req.into_inner(), &app_state).await
|
||||
#[axum::debug_handler]
|
||||
async fn xtream_player_api_get(
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
xtream_player_api(api_req, &app_state).await
|
||||
}
|
||||
|
||||
async fn xtream_player_api_post(req: HttpRequest,
|
||||
api_req: web::Form<UserApiRequest>,
|
||||
app_state: web::Data<Arc<AppState>>,
|
||||
) -> HttpResponse {
|
||||
xtream_player_api(&req, api_req.into_inner(), &app_state).await
|
||||
// async fn xtream_player_api_get(req: axum::http::Request<axum::body::Body>,
|
||||
// axum::extract::Query(api_req): axum::extract::Query<UserApiRequest>,
|
||||
// axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
// ) -> impl axum::response::IntoResponse + Send {
|
||||
// xtream_player_api(&req, api_req.into_inner(), &app_state).await
|
||||
// }
|
||||
#[axum::debug_handler]
|
||||
async fn xtream_player_api_post(
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Form(api_req): axum::extract::Form<UserApiRequest>,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
xtream_player_api(api_req, &app_state).await
|
||||
}
|
||||
// async fn xtream_player_api_post(req: axum::http::Request<axum::body::Body>,
|
||||
// api_req: web::Form<UserApiRequest>,
|
||||
// axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
// ) -> impl axum::response::IntoResponse + Send {
|
||||
// xtream_player_api(&req, api_req.into_inner(), &app_state).await
|
||||
// }
|
||||
|
||||
macro_rules! register_xtream_api {
|
||||
($cfg:expr, [$($path:expr),*]) => {{
|
||||
($router:expr, [$($path:expr),*]) => {{
|
||||
$router
|
||||
$(
|
||||
$cfg.service(web::resource($path).route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post)));
|
||||
.route($path, axum::routing::get(xtream_player_api_get))
|
||||
.route($path, axum::routing::post(xtream_player_api_post))
|
||||
// $router.service(web::resource($path).route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post)))
|
||||
)*
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! register_xtream_api_stream {
|
||||
($cfg:expr, [$(($path:expr, $fn_name:ident)),*]) => {{
|
||||
($router:expr, [$(($path:expr, $fn_name:ident)),*]) => {{
|
||||
$router
|
||||
$(
|
||||
$cfg.service(web::resource(format!("{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to($fn_name)));
|
||||
.route(format!("{}/{{username}}/{{password}}/{{stream_id}}", $path).as_str(), axum::routing::get($fn_name))
|
||||
// $cfg.service(web::resource(format!("{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to($fn_name)));
|
||||
)*
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! register_xtream_api_resource {
|
||||
($cfg:expr, [$(($path:expr, $fn_name:ident)),*]) => {{
|
||||
($router:expr, [$(($path:expr, $fn_name:ident)),*]) => {{
|
||||
$router
|
||||
$(
|
||||
$cfg.service(web::resource(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path)).route(web::get().to($fn_name)));
|
||||
.route(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path).as_str(), axum::routing::get($fn_name))
|
||||
// $cfg.service(web::resource(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path)).route(web::get().to($fn_name)));
|
||||
)*
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! register_xtream_api_timeshift {
|
||||
($cfg:expr, [$($path:expr),*]) => {{
|
||||
($router:expr, [$($path:expr),*]) => {{
|
||||
$router
|
||||
$(
|
||||
$cfg.service(web::resource($path).route(web::get().to(xtream_player_api_timeshift_stream)).route(web::post().to(xtream_player_api_timeshift_stream)));
|
||||
.route($path, axum::routing::get(xtream_player_api_timeshift_stream))
|
||||
.route($path, axum::routing::post(xtream_player_api_timeshift_stream))
|
||||
//$cfg.service(web::resource($path).route(web::get().to(xtream_player_api_timeshift_stream)).route(web::post().to(xtream_player_api_timeshift_stream)));
|
||||
)*
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn xtream_api_register(cfg: &mut web::ServiceConfig) {
|
||||
register_xtream_api!(cfg, ["/player_api.php", "/panel_api.php", "/xtream"]);
|
||||
register_xtream_api_stream!(cfg, [
|
||||
pub fn xtream_api_register() -> axum::Router<Arc<AppState>> {
|
||||
let router = axum::Router::new();
|
||||
let mut router = register_xtream_api!(router, ["/player_api.php", "/panel_api.php", "/xtream"]);
|
||||
router = register_xtream_api_stream!(router, [
|
||||
("", xtream_player_api_live_stream_alt),
|
||||
("/live", xtream_player_api_live_stream),
|
||||
("/movie", xtream_player_api_movie_stream),
|
||||
("/series", xtream_player_api_series_stream)]);
|
||||
register_xtream_api_timeshift!(cfg, [
|
||||
router = register_xtream_api_timeshift!(router, [
|
||||
"/timeshift/{username}/{password}/{duration}/{start}/{stream_id}",
|
||||
"/timeshift.php",
|
||||
"/streaming/timeshift.php"]);
|
||||
register_xtream_api_resource!(cfg, [
|
||||
register_xtream_api_resource!(router, [
|
||||
("live", xtream_player_api_live_resource),
|
||||
("movie", xtream_player_api_movie_resource),
|
||||
("series", xtream_player_api_series_resource)]);
|
||||
("series", xtream_player_api_series_resource)])
|
||||
}
|
||||
+129
-83
@@ -1,34 +1,32 @@
|
||||
use actix_cors::Cors;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, App, HttpResponse, HttpServer};
|
||||
use parking_lot::{Mutex as PlMutex};
|
||||
use log::{error, info};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mime::APPLICATION_JSON;
|
||||
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::model::app_state::{AppState, HdHomerunAppState};
|
||||
use crate::api::model::download::DownloadQueue;
|
||||
use crate::api::model::streams::shared_stream_manager::SharedStreamManager;
|
||||
use crate::api::scheduler::start_scheduler;
|
||||
use crate::api::endpoints::v1_api::v1_api_register;
|
||||
use crate::api::endpoints::web_index::index_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;
|
||||
use crate::api::model::active_user_manager::ActiveUserManager;
|
||||
use crate::api::model::app_state::{AppState, HdHomerunAppState};
|
||||
use crate::api::model::download::DownloadQueue;
|
||||
use crate::api::model::streams::shared_stream_manager::SharedStreamManager;
|
||||
use crate::api::scheduler::start_scheduler;
|
||||
use crate::model::config::{validate_targets, Config, ProcessTargets, ScheduleConfig};
|
||||
use crate::model::healthcheck::Healthcheck;
|
||||
use crate::processing::processor::playlist;
|
||||
use crate::tools::lru_cache::{LRUResourceCache};
|
||||
use crate::tools::lru_cache::LRUResourceCache;
|
||||
use crate::utils::size_utils::human_readable_byte_size;
|
||||
use crate::utils::sys_utils;
|
||||
use crate::{BUILD_TIMESTAMP, VERSION};
|
||||
use crate::api::endpoints::hdhomerun_api::{hdhr_api_register};
|
||||
use crate::api::model::active_provider_manager::ActiveProviderManager;
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::{error, info};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use axum::debug_handler;
|
||||
use tokio::sync::Mutex;
|
||||
use std::future::IntoFuture;
|
||||
|
||||
fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result<PathBuf, std::io::Error> {
|
||||
let web_dir = web_root.to_string();
|
||||
@@ -40,12 +38,17 @@ fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result<PathBuf, std
|
||||
Ok(web_dir_path)
|
||||
}
|
||||
|
||||
fn create_healthcheck(app_state: &web::Data<Arc<AppState>>) -> Healthcheck {
|
||||
async fn create_healthcheck(app_state: &Arc<AppState>) -> Healthcheck {
|
||||
let server_time = chrono::offset::Local::now().with_timezone(&chrono::Local).format("%Y-%m-%d %H:%M:%S %Z").to_string();
|
||||
let cache = app_state.cache.as_ref().as_ref().map(|c| c.lock().get_size_text());
|
||||
let (active_clients, active_connections) = {
|
||||
let cache = match app_state.cache.as_ref().as_ref() {
|
||||
None => None,
|
||||
Some(lock) => {
|
||||
Some(lock.lock().await.get_size_text())
|
||||
}
|
||||
};
|
||||
let (active_clients, active_connections) = {
|
||||
let active_user = &app_state.active_users;
|
||||
(active_user.active_users(), active_user.active_connections())
|
||||
(active_user.active_users().await, active_user.active_connections().await)
|
||||
};
|
||||
let build_time: Option<String> = BUILD_TIMESTAMP.to_string().parse::<DateTime<Utc>>().ok().map(|datetime| datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string());
|
||||
Healthcheck {
|
||||
@@ -60,27 +63,29 @@ fn create_healthcheck(app_state: &web::Data<Arc<AppState>>) -> Healthcheck {
|
||||
}
|
||||
}
|
||||
|
||||
async fn healthcheck(app_state: web::Data<Arc<AppState>>,) -> HttpResponse {
|
||||
HttpResponse::Ok().json(create_healthcheck(&app_state))
|
||||
#[debug_handler]
|
||||
async fn healthcheck(axum::extract::State(app_state): axum::extract::State<Arc<AppState>>) -> impl axum::response::IntoResponse {
|
||||
axum::Json(create_healthcheck(&app_state).await)
|
||||
}
|
||||
|
||||
async fn status(app_state: web::Data<Arc<AppState>>,) -> HttpResponse {
|
||||
let status = create_healthcheck(&app_state);
|
||||
async fn status(axum::extract::State(app_state): axum::extract::State<Arc<AppState>>) -> impl axum::response::IntoResponse {
|
||||
let status = create_healthcheck(&app_state).await;
|
||||
match serde_json::to_string_pretty(&status) {
|
||||
Ok(pretty_json) => HttpResponse::Ok().content_type(APPLICATION_JSON).body(pretty_json),
|
||||
Err(_) => HttpResponse::Ok().json(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(),
|
||||
Err(_) => axum::Json(status).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_shared_data(cfg: &Arc<Config>) -> AppState {
|
||||
let lru_cache = cfg.reverse_proxy.as_ref().and_then(|r| r.cache.as_ref()).and_then(|c| if c.enabled {
|
||||
Some(PlMutex::new(LRUResourceCache::new(c.t_size, &PathBuf::from(c.dir.as_ref().unwrap()))))
|
||||
} else { None} );
|
||||
let lru_cache = cfg.reverse_proxy.as_ref().and_then(|r| r.cache.as_ref()).and_then(|c| if c.enabled {
|
||||
Some(Mutex::new(LRUResourceCache::new(c.t_size, &PathBuf::from(c.dir.as_ref().unwrap()))))
|
||||
} else { None });
|
||||
let cache = Arc::new(lru_cache);
|
||||
let cache_scanner = Arc::clone(&cache);
|
||||
actix_rt::spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
if let Some(m) = cache_scanner.as_ref() {
|
||||
let mut c = m.lock();
|
||||
let mut c = m.lock().await;
|
||||
if let Err(err) = (*c).scan() {
|
||||
error!("Failed to scan cache {err}");
|
||||
}
|
||||
@@ -102,7 +107,7 @@ fn exec_update_on_boot(client: Arc<reqwest::Client>, cfg: &Arc<Config>, targets:
|
||||
if cfg.update_on_boot {
|
||||
let cfg_clone = Arc::clone(cfg);
|
||||
let targets_clone = Arc::clone(targets);
|
||||
actix_rt::spawn(
|
||||
tokio::spawn(
|
||||
async move { playlist::exec_processing(client, cfg_clone, targets_clone).await }
|
||||
);
|
||||
}
|
||||
@@ -145,7 +150,7 @@ fn exec_scheduler(client: &Arc<reqwest::Client>, cfg: &Arc<Config>, targets: &Ar
|
||||
let exec_targets = get_process_targets(cfg, targets, schedule.targets.as_ref());
|
||||
let cfg_clone = Arc::clone(cfg);
|
||||
let http_client = Arc::clone(client);
|
||||
actix_rt::spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
start_scheduler(http_client, expression.as_str(), cfg_clone, exec_targets).await;
|
||||
});
|
||||
}
|
||||
@@ -171,22 +176,32 @@ fn start_hdhomerun(cfg: &Arc<Config>, app_state: &Arc<AppState>, infos: &mut Vec
|
||||
let port = device.port;
|
||||
let device_clone = Arc::new(device.clone());
|
||||
infos.push(format!("HdHomeRun Server '{}' running: http://{host}:{port}", device.name));
|
||||
actix_rt::spawn(async move {
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(Cors::default()
|
||||
.supports_credentials()
|
||||
.allow_any_origin()
|
||||
.allowed_methods(vec!["GET", "POST", "OPTIONS", "HEAD"])
|
||||
.allow_any_header()
|
||||
.max_age(3600))
|
||||
.app_data(Data::new(HdHomerunAppState {
|
||||
app_state: Arc::clone(&app_data),
|
||||
device: Arc::clone(&device_clone),
|
||||
}))
|
||||
.configure(hdhr_api_register)
|
||||
}).bind(format!("{}:{port}", app_host.clone()))?.run().await
|
||||
tokio::spawn(async move {
|
||||
let cors = tower_http::cors::CorsLayer::new()
|
||||
// .allow_credentials(true)
|
||||
.allow_origin(tower_http::cors::Any)
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::OPTIONS, axum::http::Method::HEAD])
|
||||
.allow_headers(tower_http::cors::Any)
|
||||
.max_age(std::time::Duration::from_secs(3600));
|
||||
|
||||
let router = axum::Router::<Arc<HdHomerunAppState>>::new()
|
||||
.layer(cors)
|
||||
// .layer(TraceLayer::new_for_http()) // `Logger::default()`
|
||||
.merge(hdhr_api_register());
|
||||
|
||||
let router: axum::Router<()> = router.with_state(Arc::new(HdHomerunAppState {
|
||||
app_state: Arc::clone(&app_data),
|
||||
device: Arc::clone(&device_clone),
|
||||
}));
|
||||
|
||||
match tokio::net::TcpListener::bind(format!("{}:{}", app_host.clone(), port)).await {
|
||||
Ok(listener) => {
|
||||
if let Err(err) = axum::serve(listener, router).into_future().await {
|
||||
error!("{err}");
|
||||
}
|
||||
},
|
||||
Err(err) => error!("{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -194,7 +209,6 @@ fn start_hdhomerun(cfg: &Arc<Config>, app_state: &Arc<AppState>, infos: &mut Vec
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
pub async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>) -> futures::io::Result<()> {
|
||||
let mut infos = Vec::new();
|
||||
let host = cfg.api.host.to_string();
|
||||
@@ -208,13 +222,13 @@ pub async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>) -> fut
|
||||
infos.push(format!("Web root: {:?}", &web_dir_path));
|
||||
}
|
||||
let app_state = Arc::new(create_shared_data(&cfg));
|
||||
let shared_data = Data::new(Arc::clone(&app_state));
|
||||
let shared_data = Arc::clone(&app_state);
|
||||
|
||||
exec_scheduler(&Arc::clone(&shared_data.http_client), &cfg, &targets);
|
||||
exec_update_on_boot(Arc::clone(&shared_data.http_client), &cfg, &targets);
|
||||
let web_auth_enabled = is_web_auth_enabled(&cfg, web_ui_enabled);
|
||||
|
||||
if cfg.t_api_proxy.read().is_some() {
|
||||
if cfg.t_api_proxy.read().await.is_some() {
|
||||
start_hdhomerun(&cfg, &app_state, &mut infos);
|
||||
}
|
||||
|
||||
@@ -222,34 +236,66 @@ pub async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>) -> fut
|
||||
for info in &infos {
|
||||
info!("{info}");
|
||||
}
|
||||
|
||||
// Web Server
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(Cors::default()
|
||||
.supports_credentials()
|
||||
.allow_any_origin()
|
||||
.allowed_methods(vec!["GET", "POST", "OPTIONS", "HEAD"])
|
||||
.allow_any_header()
|
||||
.max_age(3600))
|
||||
.app_data(shared_data.clone())
|
||||
// .wrap(Condition::new(web_auth_enabled, ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized)))
|
||||
.configure(|srvcfg| {
|
||||
if web_ui_enabled {
|
||||
srvcfg.service(actix_files::Files::new("/static", web_dir_path.join("static")));
|
||||
srvcfg.configure(v1_api_register(web_auth_enabled));
|
||||
}
|
||||
srvcfg.service(web::resource("/healthcheck").route(web::get().to(healthcheck)));
|
||||
srvcfg.service(web::resource("/status").route(web::get().to(status)));
|
||||
})
|
||||
.configure(xtream_api_register)
|
||||
.configure(m3u_api_register)
|
||||
.configure(xmltv_api_register)
|
||||
.configure(hls_api_register)
|
||||
.configure(|srvcfg| {
|
||||
if web_ui_enabled {
|
||||
srvcfg.configure(index_register(&web_dir_path));
|
||||
}
|
||||
})
|
||||
}).bind(format!("{host}:{port}"))?.run().await
|
||||
let cors = tower_http::cors::CorsLayer::new()
|
||||
// .allow_credentials(true)
|
||||
.allow_origin(tower_http::cors::Any)
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::OPTIONS, axum::http::Method::HEAD])
|
||||
.allow_headers(tower_http::cors::Any)
|
||||
.max_age(std::time::Duration::from_secs(3600));
|
||||
|
||||
let mut router = axum::Router::new()
|
||||
.layer(cors)
|
||||
// .layer(TraceLayer::new_for_http()) // `Logger::default()`
|
||||
.route("/healthcheck", axum::routing::get(healthcheck))
|
||||
.route("/status", axum::routing::get(status));
|
||||
if web_ui_enabled {
|
||||
router = router
|
||||
.nest_service("/static", tower_http::services::ServeDir::new(web_dir_path.join("static")))
|
||||
.merge(v1_api_register(web_auth_enabled, Arc::clone(&shared_data)));
|
||||
}
|
||||
router = router
|
||||
.merge(xtream_api_register())
|
||||
.merge(m3u_api_register())
|
||||
.merge(xmltv_api_register())
|
||||
.merge(hls_api_register());
|
||||
|
||||
if web_ui_enabled {
|
||||
router = router.merge(index_register(&web_dir_path));
|
||||
}
|
||||
|
||||
let router: axum::Router<()> = router.with_state(shared_data.clone());
|
||||
let listener = tokio::net::TcpListener::bind(format!("{host}:{port}")).await?;
|
||||
axum::serve(listener, router).into_future().await
|
||||
|
||||
// HttpServer::new(move || {
|
||||
// App::new()
|
||||
// .wrap(Logger::default())
|
||||
// .wrap(Cors::default()
|
||||
// .supports_credentials()
|
||||
// .allow_any_origin()
|
||||
// .allowed_methods(vec!["GET", "POST", "OPTIONS", "HEAD"])
|
||||
// .allow_any_header()
|
||||
// .max_age(3600))
|
||||
// .app_data(shared_data.clone())
|
||||
// // .wrap(Condition::new(web_auth_enabled, ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized)))
|
||||
// .configure(|srvcfg| {
|
||||
// if web_ui_enabled {
|
||||
// srvcfg.service(actix_files::Files::new("/static", web_dir_path.join("static")));
|
||||
// srvcfg.configure(v1_api_register(web_auth_enabled));
|
||||
// }
|
||||
// srvcfg.service(web::resource("/healthcheck").route(web::get().to(healthcheck)));
|
||||
// srvcfg.service(web::resource("/status").route(web::get().to(status)));
|
||||
// })
|
||||
// .configure(xtream_api_register)
|
||||
// .configure(m3u_api_register)
|
||||
// .configure(xmltv_api_register)
|
||||
// .configure(hls_api_register)
|
||||
// .configure(|srvcfg| {
|
||||
// if web_ui_enabled {
|
||||
// srvcfg.configure(index_register(&web_dir_path));
|
||||
// }
|
||||
// })
|
||||
// }).bind(format!("{host}:{port}"))?.run().await
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::model::config::{ConfigInput, ConfigInputAlias, InputType};
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// This struct represents an individual provider configuration with fields like:
|
||||
///
|
||||
@@ -53,8 +53,8 @@ impl ProviderConfig {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_exhausted(&self) -> bool {
|
||||
self.max_connections > 0 && *self.current_connections.read() >= self.max_connections
|
||||
pub async fn is_exhausted(&self) -> bool {
|
||||
self.max_connections > 0 && *self.current_connections.read().await >= self.max_connections
|
||||
}
|
||||
//
|
||||
// #[inline]
|
||||
@@ -62,8 +62,8 @@ impl ProviderConfig {
|
||||
// !self.is_exhausted()
|
||||
// }
|
||||
|
||||
pub fn try_allocate(&self, force: bool) -> bool {
|
||||
let mut connections = self.current_connections.write();
|
||||
pub async fn try_allocate(&self, force: bool) -> bool {
|
||||
let mut connections = self.current_connections.write().await;
|
||||
if force || *connections < self.max_connections {
|
||||
*connections += 1;
|
||||
return true;
|
||||
@@ -71,8 +71,8 @@ impl ProviderConfig {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn release(&self) {
|
||||
let mut connections = self.current_connections.write();
|
||||
pub async fn release(&self) {
|
||||
let mut connections = self.current_connections.write().await;
|
||||
if *connections > 0 {
|
||||
*connections -= 1;
|
||||
}
|
||||
@@ -90,17 +90,17 @@ enum ProviderLineup {
|
||||
}
|
||||
|
||||
impl ProviderLineup {
|
||||
fn acquire(&self, force: bool) -> Option<&ProviderConfig> {
|
||||
async fn acquire(&self, force: bool) -> Option<&ProviderConfig> {
|
||||
match self {
|
||||
ProviderLineup::Single(lineup) => lineup.acquire(force),
|
||||
ProviderLineup::Multi(lineup) => lineup.acquire(force),
|
||||
ProviderLineup::Single(lineup) => lineup.acquire(force).await,
|
||||
ProviderLineup::Multi(lineup) => lineup.acquire(force).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&self, provider_id: u16) {
|
||||
async fn release(&self, provider_id: u16) {
|
||||
match self {
|
||||
ProviderLineup::Single(lineup) => lineup.release(provider_id),
|
||||
ProviderLineup::Multi(lineup) => lineup.release(provider_id),
|
||||
ProviderLineup::Single(lineup) => lineup.release(provider_id).await,
|
||||
ProviderLineup::Multi(lineup) => lineup.release(provider_id).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,17 +118,17 @@ impl SingleProviderLineup {
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire(&self, force: bool) -> Option<&ProviderConfig> {
|
||||
if self.provider.try_allocate(force) {
|
||||
async fn acquire(&self, force: bool) -> Option<&ProviderConfig> {
|
||||
if self.provider.try_allocate(force).await {
|
||||
Some(&self.provider)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&self, provider_id: u16) {
|
||||
async fn release(&self, provider_id: u16) {
|
||||
if self.provider.id == provider_id {
|
||||
self.provider.release();
|
||||
self.provider.release().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,10 +145,17 @@ enum ProviderPriorityGroup {
|
||||
}
|
||||
|
||||
impl ProviderPriorityGroup {
|
||||
fn is_exhausted(&self) -> bool {
|
||||
async fn is_exhausted(&self) -> bool {
|
||||
match self {
|
||||
ProviderPriorityGroup::SingleProviderGroup(g) => g.is_exhausted(),
|
||||
ProviderPriorityGroup::MultiProviderGroup(_, groups) => groups.iter().all(ProviderConfig::is_exhausted)
|
||||
ProviderPriorityGroup::SingleProviderGroup(g) => g.is_exhausted().await,
|
||||
ProviderPriorityGroup::MultiProviderGroup(_, groups) => {
|
||||
for g in groups {
|
||||
if !g.is_exhausted().await {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,10 +228,10 @@ impl MultiProviderLineup {
|
||||
/// println!("No available providers in group 0.");
|
||||
/// }
|
||||
/// ```
|
||||
fn acquire_next_provider_from_group(priority_group: &ProviderPriorityGroup) -> Option<&ProviderConfig> {
|
||||
async fn acquire_next_provider_from_group(priority_group: &ProviderPriorityGroup) -> Option<&ProviderConfig> {
|
||||
match priority_group {
|
||||
ProviderPriorityGroup::SingleProviderGroup(p) => {
|
||||
if p.try_allocate(false) {
|
||||
if p.try_allocate(false).await {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
@@ -234,7 +241,7 @@ impl MultiProviderLineup {
|
||||
for _ in 0..provider_count {
|
||||
let p = pg.get(idx).unwrap();
|
||||
idx = (idx + 1) % provider_count;
|
||||
if p.try_allocate(false) {
|
||||
if p.try_allocate(false).await {
|
||||
index.store(idx, Ordering::SeqCst);
|
||||
return Some(p);
|
||||
}
|
||||
@@ -275,15 +282,15 @@ impl MultiProviderLineup {
|
||||
/// println!("No available providers.");
|
||||
/// }
|
||||
/// ```
|
||||
fn acquire(&self, force: bool) -> Option<&ProviderConfig> {
|
||||
async fn acquire(&self, force: bool) -> Option<&ProviderConfig> {
|
||||
let mut main_idx = self.index.load(Ordering::SeqCst);
|
||||
let provider_count = self.providers.len();
|
||||
|
||||
for _ in 0..provider_count {
|
||||
let priority_group = &self.providers[main_idx];
|
||||
main_idx = (main_idx + 1) % provider_count;
|
||||
if let Some(provider) = Self::acquire_next_provider_from_group(priority_group) {
|
||||
if priority_group.is_exhausted() {
|
||||
if let Some(provider) = Self::acquire_next_provider_from_group(priority_group).await {
|
||||
if priority_group.is_exhausted().await {
|
||||
self.index.store(main_idx, Ordering::SeqCst);
|
||||
}
|
||||
return Some(provider);
|
||||
@@ -308,19 +315,19 @@ impl MultiProviderLineup {
|
||||
}
|
||||
|
||||
|
||||
fn release(&self, provider_id: u16) {
|
||||
async fn release(&self, provider_id: u16) {
|
||||
for g in &self.providers {
|
||||
match g {
|
||||
ProviderPriorityGroup::SingleProviderGroup(pc) => {
|
||||
if pc.id == provider_id {
|
||||
pc.release();
|
||||
pc.release().await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ProviderPriorityGroup::MultiProviderGroup(_, group) => {
|
||||
for pc in group {
|
||||
if pc.id == provider_id {
|
||||
pc.release();
|
||||
pc.release().await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -328,6 +335,7 @@ impl MultiProviderLineup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct ActiveProviderManager {
|
||||
@@ -352,13 +360,16 @@ impl ActiveProviderManager {
|
||||
self.providers.insert(name.to_string(), lineup);
|
||||
}
|
||||
|
||||
pub fn acquire_connection(&self, lineup_name: &str) -> Option<&ProviderConfig> {
|
||||
self.providers.get(lineup_name).and_then(|lineup| lineup.acquire(self.user_access_control))
|
||||
pub async fn acquire_connection(&self, lineup_name: &str) -> Option<&ProviderConfig> {
|
||||
match self.providers.get(lineup_name) {
|
||||
None => None,
|
||||
Some(lineup) => lineup.acquire(self.user_access_control).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release_connection(&self, lineup_name: &str, provider_id: u16) {
|
||||
pub async fn release_connection(&self, lineup_name: &str, provider_id: u16) {
|
||||
if let Some(lineup) = self.providers.get(lineup_name) {
|
||||
lineup.release(provider_id);
|
||||
lineup.release(provider_id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use parking_lot::RwLock;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct ActiveUserManager {
|
||||
pub user: RwLock<HashMap<String, AtomicU32>>,
|
||||
@@ -19,44 +19,40 @@ impl ActiveUserManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_connections(&self, username: &str) -> u32 {
|
||||
if let Some(counter) = self.user.read().get(username) {
|
||||
pub async fn user_connections(&self, username: &str) -> u32 {
|
||||
if let Some(counter) = self.user.read().await.get(username) {
|
||||
return counter.load(std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
pub fn active_users(&self) -> usize {
|
||||
self.user.read().len()
|
||||
pub async fn active_users(&self) -> usize {
|
||||
self.user.read().await.len()
|
||||
}
|
||||
|
||||
pub fn active_connections(&self) -> usize {
|
||||
self.user.read().values().map(|c| c.load(Ordering::SeqCst) as usize).sum()
|
||||
pub async fn active_connections(&self) -> usize {
|
||||
self.user.read().await.values().map(|c| c.load(Ordering::SeqCst) as usize).sum()
|
||||
}
|
||||
|
||||
pub fn add_connection(&self, username: &str) -> (usize, usize) {
|
||||
{
|
||||
let mut lock = self.user.write();
|
||||
if let Some(counter) = lock.get(username) {
|
||||
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
} else {
|
||||
lock.insert(username.to_string(), AtomicU32::new(1));
|
||||
}
|
||||
drop(lock);
|
||||
pub async fn add_connection(&self, username: &str) -> (usize, usize) {
|
||||
let mut lock = self.user.write().await;
|
||||
if let Some(counter) = lock.get(username) {
|
||||
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
} else {
|
||||
lock.insert(username.to_string(), AtomicU32::new(1));
|
||||
}
|
||||
(self.active_users(), self.active_connections())
|
||||
drop(lock);
|
||||
(self.active_users().await, self.active_connections().await)
|
||||
}
|
||||
|
||||
pub fn remove_connection(&self, username: &str) -> (usize, usize) {
|
||||
{
|
||||
let mut lock = self.user.write();
|
||||
if let Some(counter) = lock.get(username) {
|
||||
if counter.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) == 1 {
|
||||
lock.remove(username);
|
||||
}
|
||||
pub async fn remove_connection(&self, username: &str) -> (usize, usize) {
|
||||
let mut lock = self.user.write().await;
|
||||
if let Some(counter) = lock.get(username) {
|
||||
if counter.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) == 1 {
|
||||
lock.remove(username);
|
||||
}
|
||||
drop(lock);
|
||||
}
|
||||
(self.active_users(), self.active_connections())
|
||||
drop(lock);
|
||||
(self.active_users().await, self.active_connections().await)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::sync::{Arc};
|
||||
use parking_lot::{Mutex};
|
||||
use tokio::sync::{Mutex};
|
||||
use std::sync::Arc;
|
||||
use crate::api::model::active_provider_manager::ActiveProviderManager;
|
||||
use crate::api::model::active_user_manager::ActiveUserManager;
|
||||
use crate::api::model::download::DownloadQueue;
|
||||
@@ -8,6 +8,8 @@ use crate::model::config::{Config};
|
||||
use crate::model::hdhomerun_config::HdHomeRunDeviceConfig;
|
||||
use crate::tools::lru_cache::LRUResourceCache;
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub config: Arc<Config>,
|
||||
pub http_client: Arc<reqwest::Client>,
|
||||
@@ -19,11 +21,12 @@ pub struct AppState {
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn get_active_connections_for_user(&self, username: &str) -> u32 {
|
||||
self.active_users.user_connections(username)
|
||||
pub async fn get_active_connections_for_user(&self, username: &str) -> u32 {
|
||||
self.active_users.user_connections(username).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HdHomerunAppState {
|
||||
pub app_state: Arc<AppState>,
|
||||
pub device: Arc<HdHomeRunDeviceConfig>,
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::sync::{RwLock, Mutex};
|
||||
use std::sync::Arc;
|
||||
use actix_web::web;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unidecode::unidecode;
|
||||
|
||||
@@ -141,9 +140,3 @@ pub struct FileDownloadRequest {
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
impl From<web::Json<Self>> for FileDownloadRequest {
|
||||
fn from(req: web::Json<Self>) -> Self {
|
||||
req.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::utils::debug_if_enabled;
|
||||
use actix_web::http::header::{HeaderName, HeaderValue};
|
||||
use actix_web::{HttpResponseBuilder};
|
||||
use reqwest::{StatusCode};
|
||||
use std::collections::{HashSet};
|
||||
use std::str::FromStr;
|
||||
@@ -16,35 +14,45 @@ pub fn get_response_headers(headers: &HeaderMap) -> Vec<(String, String)> {
|
||||
response_headers
|
||||
}
|
||||
|
||||
pub fn get_stream_response_with_headers(custom: Option<(Vec<(String, String)>, StatusCode)>, stream_url: &str) -> HttpResponseBuilder {
|
||||
let mut headers = Vec::<(HeaderName, HeaderValue)>::with_capacity(12);
|
||||
pub fn get_stream_response_with_headers(custom: Option<(Vec<(String, String)>, StatusCode)>, stream_url: &str) -> (axum::http::StatusCode, axum::http::HeaderMap) {
|
||||
let mut headers = HeaderMap::new();
|
||||
let mut added_headers: HashSet<String> = HashSet::new();
|
||||
let mut status = 200_u16;
|
||||
let mut status = StatusCode::OK;
|
||||
|
||||
if let Some((custom_headers, status_code)) = custom {
|
||||
status = status_code.as_u16();
|
||||
for header in custom_headers {
|
||||
headers.push((HeaderName::from_str(&header.0).unwrap(), HeaderValue::from_str(header.1.as_str()).unwrap()));
|
||||
added_headers.insert(header.0.to_string());
|
||||
status = status_code;
|
||||
for (key, value) in custom_headers {
|
||||
if let (Ok(name), Ok(val)) = (axum::http::HeaderName::from_str(&key), axum::http::HeaderValue::from_str(&value)) {
|
||||
headers.insert(name.clone(), val);
|
||||
added_headers.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let default_headers = vec![
|
||||
(actix_web::http::header::CONTENT_TYPE, HeaderValue::from_str("application/octet-stream").unwrap()),
|
||||
(actix_web::http::header::CONNECTION, HeaderValue::from_str("keep-alive").unwrap()),
|
||||
("content-type", "application/octet-stream"),
|
||||
("connection", "keep-alive"),
|
||||
];
|
||||
|
||||
for header in default_headers {
|
||||
if !added_headers.contains(header.0.as_str()) {
|
||||
headers.push(header);
|
||||
for (key, value) in default_headers {
|
||||
if !added_headers.contains(key) {
|
||||
if let (Ok(name), Ok(val)) = (axum::http::HeaderName::from_str(key), axum::http::HeaderValue::from_str(value)) {
|
||||
headers.insert(name, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headers.push((actix_web::http::header::DATE, HeaderValue::from_str(&chrono::Utc::now().to_rfc2822()).unwrap()));
|
||||
|
||||
let mut response_builder = actix_web::HttpResponse::build(actix_web::http::StatusCode::from_u16(status).unwrap());
|
||||
debug_if_enabled!("Responding stream request {} with status {status}, headers {headers:?}", sanitize_sensitive_info(stream_url));
|
||||
for header in headers {
|
||||
response_builder.insert_header(header);
|
||||
// Füge das aktuelle Datum hinzu
|
||||
if let Ok(date_header) = axum::http::HeaderValue::from_str(&chrono::Utc::now().to_rfc2822()) {
|
||||
headers.insert(axum::http::HeaderName::from_static("date"), date_header);
|
||||
}
|
||||
response_builder
|
||||
|
||||
debug_if_enabled!(
|
||||
"Responding stream request {} with status {}, headers {:?}",
|
||||
sanitize_sensitive_info(stream_url),
|
||||
status,
|
||||
headers
|
||||
);
|
||||
|
||||
(status, headers)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use actix_web::web;
|
||||
use serde::de::{self, Deserializer, Unexpected};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -43,12 +42,6 @@ pub struct PlaylistRequest {
|
||||
pub source_name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<web::Json<Self>> for PlaylistRequest {
|
||||
fn from(req: web::Json<Self>) -> Self {
|
||||
req.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct UserApiRequest {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -6,6 +6,7 @@ pub enum StreamError {
|
||||
// StdIo(std::io::Error),
|
||||
// ReceiverClosed,
|
||||
ReceiverError(BroadcastStreamRecvError),
|
||||
LockError(String)
|
||||
}
|
||||
|
||||
impl StreamError {
|
||||
@@ -26,6 +27,7 @@ impl std::fmt::Display for StreamError {
|
||||
// StreamError::StdIo(e) => write!(f, "IO error: {e}"),
|
||||
// StreamError::ReceiverClosed => write!(f, "Receiver closed"),
|
||||
StreamError::ReceiverError(e) => write!(f, "Receiver error {e}"),
|
||||
StreamError::LockError(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ pub(in crate::api) struct ActiveClientStream {
|
||||
}
|
||||
|
||||
impl ActiveClientStream {
|
||||
pub(crate) fn new(inner: ResponseStream, active_clients: Arc<ActiveUserManager>, user: &ProxyUserCredentials, log_active_clients: bool) -> Self {
|
||||
let (client_count, connection_count) = active_clients.add_connection(&user.username);
|
||||
pub(crate) async fn new(inner: ResponseStream, active_clients: Arc<ActiveUserManager>, user: &ProxyUserCredentials, log_active_clients: bool) -> Self {
|
||||
let (client_count, connection_count) = active_clients.add_connection(&user.username).await;
|
||||
if log_active_clients {
|
||||
info!("Active clients: {client_count}, active connections {connection_count}");
|
||||
}
|
||||
@@ -36,9 +36,16 @@ impl Stream for ActiveClientStream {
|
||||
|
||||
impl Drop for ActiveClientStream {
|
||||
fn drop(&mut self) {
|
||||
let (client_count, connection_count) = self.active_clients.remove_connection(&self.username);
|
||||
if self.log_active_clients {
|
||||
info!("Active clients: {client_count}, active connections {connection_count}");
|
||||
}
|
||||
let username = self.username.clone();
|
||||
let log_active_clients = self.log_active_clients;
|
||||
let active_clients = Arc::clone(&self.active_clients);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let username = username.clone();
|
||||
let (client_count, connection_count) = active_clients.remove_connection(&username).await;
|
||||
if log_active_clients {
|
||||
info!("Active clients: {client_count}, active connections {connection_count}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub(in crate::api::model) struct BufferedStream {
|
||||
impl BufferedStream {
|
||||
pub fn new(stream: ResponseStream, buffer_size: usize, client_close_signal: Arc<AtomicOnceFlag>, _url: &str) -> Self {
|
||||
let (tx, rx) = channel(buffer_size);
|
||||
actix_rt::spawn(Self::buffer_stream(tx, stream, client_close_signal));
|
||||
tokio::spawn(Self::buffer_stream(tx, stream, client_close_signal));
|
||||
Self {
|
||||
stream: ReceiverStream::new(rx)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ impl BufferedStream {
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
trace!("Buffered Stream Error: {err:?}");
|
||||
// actix_web::rt::time::sleep(sleep_duration).await;
|
||||
// tokio::time::sleep(sleep_duration).await;
|
||||
// Attempt to send error to client
|
||||
if tx.send(Err(err)).await.is_err() {
|
||||
client_close_signal.notify();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::io::Write;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::task::{Context, Poll};
|
||||
use bytes::Bytes;
|
||||
@@ -26,7 +27,7 @@ pub struct PersistPipeStream<S, W> {
|
||||
completed: bool,
|
||||
writer: W,
|
||||
size: AtomicUsize,
|
||||
callback: Box<dyn Fn(usize)>,
|
||||
callback: Arc<dyn Fn(usize) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<S, W> PersistPipeStream<S, W>
|
||||
@@ -39,7 +40,7 @@ where
|
||||
/// - `inner`: The input stream providing the data.
|
||||
/// - `writer`: The writer to which the data is written.
|
||||
/// - `callback`: A callback function to be called with the total size upon stream completion.
|
||||
pub fn new(inner: S, writer: W, callback: Box<dyn Fn(usize)>) -> Self {
|
||||
pub fn new(inner: S, writer: W, callback: Arc<dyn Fn(usize) + Send + Sync>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
completed: false,
|
||||
|
||||
@@ -7,13 +7,13 @@ use crate::model::config::{Config, ConfigInput};
|
||||
use crate::model::playlist::PlaylistItemType;
|
||||
use crate::utils::debug_if_enabled;
|
||||
use crate::utils::network::request::{get_request_headers, sanitize_sensitive_info};
|
||||
use actix_web::HttpRequest;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::BoxStream;
|
||||
use futures::TryStreamExt;
|
||||
use log::{debug, error};
|
||||
use reqwest::StatusCode;
|
||||
use std::sync::Arc;
|
||||
use axum::http::HeaderMap;
|
||||
use url::Url;
|
||||
|
||||
type BoxedProviderStream = BoxStream<'static, Result<Bytes, StreamError>>;
|
||||
@@ -45,11 +45,11 @@ pub fn get_header_filter_for_item_type(item_type: PlaylistItemType) -> HeaderFil
|
||||
pub async fn get_provider_pipe_stream(cfg: &Config,
|
||||
http_client: &Arc<reqwest::Client>,
|
||||
stream_url: &Url,
|
||||
req: &HttpRequest,
|
||||
req_headers: &HeaderMap,
|
||||
input: Option<&ConfigInput>,
|
||||
item_type: PlaylistItemType) -> ProviderStreamResponse {
|
||||
let filter_header = get_header_filter_for_item_type(item_type);
|
||||
let req_headers = get_headers_from_request(req, &filter_header);
|
||||
let req_headers = get_headers_from_request(req_headers, &filter_header);
|
||||
debug_if_enabled!("Stream requested with headers: {:?}", req_headers.iter().map(|header| (header.0, String::from_utf8_lossy(header.1))).collect::<Vec<_>>());
|
||||
// These are the configured headers for this input.
|
||||
let input_headers = input.map(|i| i.headers.clone());
|
||||
@@ -84,10 +84,10 @@ pub async fn get_provider_pipe_stream(cfg: &Config,
|
||||
pub async fn get_provider_reconnect_buffered_stream(cfg: &Config,
|
||||
http_client: &Arc<reqwest::Client>,
|
||||
stream_url: &Url,
|
||||
req: &HttpRequest,
|
||||
req_headers: &HeaderMap,
|
||||
input: Option<&ConfigInput>,
|
||||
options: BufferStreamOptions) -> ProviderStreamResponse {
|
||||
match create_provider_stream(cfg, Arc::clone(http_client), stream_url, req, input, options).await {
|
||||
match create_provider_stream(cfg, Arc::clone(http_client), stream_url, req_headers, input, options).await {
|
||||
None => (None, None),
|
||||
Some((stream, info)) => {
|
||||
(Some(stream), info)
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::model::playlist::PlaylistItemType;
|
||||
use crate::tools::atomic_once_flag::AtomicOnceFlag;
|
||||
use crate::utils::debug_if_enabled;
|
||||
use crate::utils::network::request::{classify_content_type, get_request_headers, sanitize_sensitive_info, MimeCategory};
|
||||
use actix_web::HttpRequest;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
@@ -153,7 +152,7 @@ impl ProviderStreamOptions {
|
||||
|
||||
fn get_request_range_start_bytes(req_headers: &HashMap<String, Vec<u8>>) -> Option<usize> {
|
||||
// range header looks like bytes=1234-5566/2345345 or bytes=0-
|
||||
if let Some(req_range) = req_headers.get(actix_web::http::header::RANGE.as_str()) {
|
||||
if let Some(req_range) = req_headers.get(axum::http::header::RANGE.as_str()) {
|
||||
if let Some(bytes_range) = req_range.strip_prefix(b"bytes=") {
|
||||
if let Some(index) = bytes_range.iter().position(|&x| x == b'-') {
|
||||
let start_bytes = &bytes_range[..index];
|
||||
@@ -169,13 +168,13 @@ fn get_request_range_start_bytes(req_headers: &HashMap<String, Vec<u8>>) -> Opti
|
||||
}
|
||||
|
||||
fn get_client_stream_request_params(
|
||||
req: &HttpRequest,
|
||||
req_headers: &HeaderMap,
|
||||
input: Option<&ConfigInput>,
|
||||
options: &BufferStreamOptions) -> (usize, Option<usize>, bool, HeaderMap)
|
||||
{
|
||||
let stream_buffer_size = if options.is_buffer_enabled() { options.get_stream_buffer_size() } else { 1 };
|
||||
let filter_header = get_header_filter_for_item_type(options.item_type);
|
||||
let mut req_headers = get_headers_from_request(req, &filter_header);
|
||||
let mut req_headers = get_headers_from_request(req_headers, &filter_header);
|
||||
debug_if_enabled!("Stream requested with headers: {:?}", req_headers.iter().map(|header| (header.0, String::from_utf8_lossy(header.1))).collect::<Vec<_>>());
|
||||
// we need the range bytes from client request for seek ing to the right position
|
||||
let req_range_start_bytes = get_request_range_start_bytes(&req_headers);
|
||||
@@ -277,7 +276,7 @@ async fn stream_provider(client: Arc<reqwest::Client>, stream_options: ProviderS
|
||||
if !stream_options.should_continue() {
|
||||
return None;
|
||||
}
|
||||
actix_web::rt::time::sleep(Duration::from_millis(100)).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
debug_if_enabled!("Stopped reconnecting stream {}", sanitize_sensitive_info(url.as_str()));
|
||||
None
|
||||
@@ -310,17 +309,17 @@ async fn get_initial_stream(cfg: &Config, client: Arc<reqwest::Client>, stream_o
|
||||
break;
|
||||
}
|
||||
connect_err += 1;
|
||||
actix_web::rt::time::sleep(Duration::from_millis(100)).await;
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
stream_options.cancel_reconnect();
|
||||
None
|
||||
}
|
||||
|
||||
fn create_provider_stream_options(stream_url: &Url,
|
||||
req: &HttpRequest,
|
||||
req_headers: &HeaderMap,
|
||||
input: Option<&ConfigInput>,
|
||||
options: &BufferStreamOptions) -> ProviderStreamOptions {
|
||||
let (buffer_size, req_range_start_bytes, reconnect, headers) = get_client_stream_request_params(req, input, options);
|
||||
let (buffer_size, req_range_start_bytes, reconnect, headers) = get_client_stream_request_params(req_headers, input, options);
|
||||
let url = stream_url.clone();
|
||||
let range_bytes = Arc::new(req_range_start_bytes.map(AtomicUsize::new));
|
||||
let continue_flag = Arc::new(AtomicOnceFlag::new());
|
||||
@@ -338,10 +337,10 @@ fn create_provider_stream_options(stream_url: &Url,
|
||||
pub async fn create_provider_stream(cfg: &Config,
|
||||
client: Arc<reqwest::Client>,
|
||||
stream_url: &Url,
|
||||
req: &HttpRequest,
|
||||
req_headers: &HeaderMap,
|
||||
input: Option<&ConfigInput>,
|
||||
options: BufferStreamOptions) -> Option<ProviderStreamFactoryResponse> {
|
||||
let stream_options = create_provider_stream_options(stream_url, req, input, &options);
|
||||
let stream_options = create_provider_stream_options(stream_url, req_headers, input, &options);
|
||||
|
||||
let client_stream_factory = |stream, reconnect_flag, range_cnt| {
|
||||
let stream = if stream_options.is_buffered() && !options.is_shared_stream() {
|
||||
@@ -407,7 +406,7 @@ pub async fn create_provider_stream(cfg: &Config,
|
||||
// let req = TestRequest::get().uri("/test").to_request();
|
||||
// let _response = test::call_service(&server, req).await;
|
||||
// }
|
||||
// async fn test_stream_handler(req: HttpRequest) -> HttpResponse {
|
||||
// async fn test_stream_handler(req: axum::http::Request<axum::body::Body>) -> impl axum::response::IntoResponse + Send {
|
||||
// let cfg = Config::default();
|
||||
// let mut counter = 5;
|
||||
// let client = Arc::new(reqwest::Client::new());
|
||||
|
||||
@@ -3,12 +3,12 @@ use crate::api::model::streams::provider_stream_factory::STREAM_QUEUE_SIZE;
|
||||
use crate::api::model::stream_error::StreamError;
|
||||
use crate::utils::debug_if_enabled;
|
||||
use crate::utils::network::request::sanitize_sensitive_info;
|
||||
use parking_lot::{RwLock};
|
||||
use bytes::Bytes;
|
||||
use futures::stream::BoxStream;
|
||||
use futures::{Stream, StreamExt};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::mpsc::{Sender};
|
||||
|
||||
use std::pin::Pin;
|
||||
@@ -73,16 +73,16 @@ impl SharedStreamState {
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> BoxStream<'static, Result<Bytes, StreamError>> {
|
||||
async fn subscribe(&self) -> BoxStream<'static, Result<Bytes, StreamError>> {
|
||||
let (tx, rx) = mpsc::channel(self.buf_size);
|
||||
self.subscribers.write().push(tx);
|
||||
self.subscribers.write().await.push(tx);
|
||||
convert_stream(ReceiverStream::new(rx).boxed())
|
||||
}
|
||||
|
||||
fn broadcast<S, E>(&self, stream_url: &str, bytes_stream: S, shared_streams: Arc<SharedStreamManager>)
|
||||
where
|
||||
S: Stream<Item=Result<Bytes, E>> + Unpin + 'static,
|
||||
E: std::fmt::Debug
|
||||
S: Stream<Item=Result<Bytes, E>> + Unpin + 'static + std::marker::Send,
|
||||
E: std::fmt::Debug + std::marker::Send
|
||||
{
|
||||
let starving_size = self.buf_size-4;
|
||||
let sleep_duration = Duration::from_millis(10);
|
||||
@@ -92,28 +92,28 @@ impl SharedStreamState {
|
||||
let streaming_url = stream_url.to_string();
|
||||
|
||||
//Spawn a task to forward items from the source stream to the broadcast channel
|
||||
actix_rt::spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
while let Some(item) = source_stream.next().await {
|
||||
if let Ok(data) = item {
|
||||
if subscriber.read().is_empty() {
|
||||
if subscriber.read().await.is_empty() {
|
||||
debug_if_enabled!("No active subscribers. Closing shared provider stream {}", sanitize_sensitive_info(&streaming_url));
|
||||
// Cleanup for removing unused shared streams
|
||||
shared_streams.unregister(&streaming_url);
|
||||
shared_streams.unregister(&streaming_url).await;
|
||||
break;
|
||||
}
|
||||
|
||||
let start_time = Instant::now();
|
||||
loop {
|
||||
if subscriber.read().iter().any(|sender| sender.capacity() >= starving_size) {
|
||||
if subscriber.read().await.iter().any(|sender| sender.capacity() >= starving_size) {
|
||||
break;
|
||||
}
|
||||
actix_web::rt::time::sleep(sleep_duration_starve).await;
|
||||
tokio::time::sleep(sleep_duration_starve).await;
|
||||
if start_time.elapsed().as_secs() > 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut subs = subscriber.write();
|
||||
let mut subs = subscriber.write().await;
|
||||
// TODO use drain_filter
|
||||
(*subs).retain(|sender| {
|
||||
match sender.try_send(data.clone()) {
|
||||
@@ -126,10 +126,10 @@ impl SharedStreamState {
|
||||
}
|
||||
});
|
||||
}
|
||||
actix_web::rt::time::sleep(sleep_duration).await;
|
||||
tokio::time::sleep(sleep_duration).await;
|
||||
}
|
||||
debug_if_enabled!("Shared stream exhausted. Closing shared provider stream {}", sanitize_sensitive_info(&streaming_url));
|
||||
shared_streams.unregister(&streaming_url);
|
||||
shared_streams.unregister(&streaming_url).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -147,46 +147,46 @@ impl SharedStreamManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_shared_state_headers(&self, stream_url: &str) -> Option<Vec<(String, String)>> {
|
||||
self.shared_streams.read().get(stream_url).map(|s| s.headers.clone())
|
||||
pub async fn get_shared_state_headers(&self, stream_url: &str) -> Option<Vec<(String, String)>> {
|
||||
self.shared_streams.read().await.get(stream_url).map(|s| s.headers.clone())
|
||||
}
|
||||
|
||||
fn unregister(&self, stream_url: &str) {
|
||||
self.shared_streams.write().remove(stream_url);
|
||||
async fn unregister(&self, stream_url: &str) {
|
||||
let _ = self.shared_streams.write().await.remove(stream_url);
|
||||
}
|
||||
|
||||
fn subscribe_stream(&self, stream_url: &str) -> Option<BoxStream<'static, Result<Bytes, StreamError>>> {
|
||||
let stream_data = self.shared_streams.read().get(stream_url)?.subscribe();
|
||||
async fn subscribe_stream(&self, stream_url: &str) -> Option<BoxStream<'static, Result<Bytes, StreamError>>> {
|
||||
let stream_data = self.shared_streams.read().await.get(stream_url)?.subscribe().await;
|
||||
Some(stream_data)
|
||||
}
|
||||
|
||||
fn register(&self, stream_url: &str, shared_state: SharedStreamState) {
|
||||
self.shared_streams.write().insert(stream_url.to_string(), shared_state);
|
||||
async fn register(&self, stream_url: &str, shared_state: SharedStreamState) {
|
||||
let _= self.shared_streams.write().await.insert(stream_url.to_string(), shared_state);
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe<S, E>(
|
||||
pub(crate) async fn subscribe<S, E>(
|
||||
app_state: &AppState,
|
||||
stream_url: &str,
|
||||
bytes_stream: S,
|
||||
headers: Vec<(String, String)>,
|
||||
buffer_size: usize,)
|
||||
where
|
||||
S: Stream<Item=Result<Bytes, E>> + Unpin + 'static,
|
||||
E: std::fmt::Debug
|
||||
S: Stream<Item=Result<Bytes, E>> + Unpin + 'static + std::marker::Send,
|
||||
E: std::fmt::Debug + std::marker::Send
|
||||
{
|
||||
let buf_size = std::cmp::max(buffer_size, STREAM_QUEUE_SIZE);
|
||||
let shared_state = SharedStreamState::new(headers, buf_size);
|
||||
shared_state.broadcast(stream_url, bytes_stream, Arc::clone(&app_state.shared_stream_manager));
|
||||
app_state.shared_stream_manager.register(stream_url, shared_state);
|
||||
app_state.shared_stream_manager.register(stream_url, shared_state).await;
|
||||
debug_if_enabled!("Created shared provider stream {}", sanitize_sensitive_info(stream_url));
|
||||
}
|
||||
|
||||
/// Creates a broadcast notify stream for the given URL if a shared stream exists.
|
||||
pub fn subscribe_shared_stream(
|
||||
pub async fn subscribe_shared_stream(
|
||||
app_state: &AppState,
|
||||
stream_url: &str,
|
||||
) -> Option<BoxStream<'static, Result<Bytes, StreamError>>> {
|
||||
debug_if_enabled!("Responding existing shared client stream {}", sanitize_sensitive_info(stream_url));
|
||||
app_state.shared_stream_manager.subscribe_stream(stream_url)
|
||||
app_state.shared_stream_manager.subscribe_stream(stream_url).await
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials, ProxyUserStatus};
|
||||
use chrono::{Duration, Local};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct XtreamUserInfoResponse {
|
||||
pub password: String,
|
||||
pub username: String,
|
||||
@@ -17,7 +17,7 @@ pub struct XtreamUserInfoResponse {
|
||||
pub status: String, // "Active"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct XtreamServerInfoResponse {
|
||||
pub url: String,
|
||||
pub port: String,
|
||||
@@ -29,7 +29,7 @@ pub struct XtreamServerInfoResponse {
|
||||
pub time_now: String, //"2021-06-28 17:07:37"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct XtreamAuthorizationResponse {
|
||||
pub user_info: XtreamUserInfoResponse,
|
||||
pub server_info: XtreamServerInfoResponse,
|
||||
|
||||
@@ -31,7 +31,7 @@ pub async fn start_scheduler(client: Arc<reqwest::Client>, expression: &str, con
|
||||
loop {
|
||||
let mut upcoming = schedule.upcoming(offset).take(1);
|
||||
if let Some(datetime) = upcoming.next() {
|
||||
actix_web::rt::time::sleep_until(actix_rt::time::Instant::from(datetime_to_instant(datetime))).await;
|
||||
tokio::time::sleep_until(tokio::time::Instant::from(datetime_to_instant(datetime))).await;
|
||||
exec_processing(Arc::clone(&client), Arc::clone(&config), Arc::clone(&targets)).await;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ mod tests {
|
||||
loop {
|
||||
let mut upcoming = schedule.upcoming(offset).take(1);
|
||||
if let Some(datetime) = upcoming.next() {
|
||||
actix_web::rt::time::sleep_until(actix_rt::time::Instant::from(datetime_to_instant(datetime))).await;
|
||||
tokio::time::sleep_until(actix_rt::time::Instant::from(datetime_to_instant(datetime))).await;
|
||||
run_me();
|
||||
}
|
||||
if runs.load(Ordering::SeqCst) == 6 {
|
||||
|
||||
+36
-38
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use actix_web::{dev::ServiceRequest, Error, web};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use axum::debug_middleware;
|
||||
use chrono::{Local, Duration};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, encode, decode, EncodingKey, Header, Validation, TokenData};
|
||||
use crate::model::config::WebAuthConfig;
|
||||
@@ -46,12 +45,9 @@ fn create_jwt(web_auth_config: &WebAuthConfig, username: &str, roles: Vec<String
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verify_token(bearer: Option<BearerAuth>, secret_key: &[u8]) -> Option<TokenData<Claims>> {
|
||||
if let Some(auth) = bearer {
|
||||
let token = auth.token();
|
||||
if let Ok(token_data) = decode::<Claims>(token, &DecodingKey::from_secret(secret_key), &Validation::new(Algorithm::HS256)) {
|
||||
return Some(token_data);
|
||||
}
|
||||
pub(crate) fn verify_token(token: &str, secret_key: &[u8]) -> Option<TokenData<Claims>> {
|
||||
if let Ok(token_data) = decode::<Claims>(token, &DecodingKey::from_secret(secret_key), &Validation::new(Algorithm::HS256)) {
|
||||
return Some(token_data);
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -72,49 +68,51 @@ pub fn is_user(token_data: Option<TokenData<Claims>>) -> bool {
|
||||
has_role(token_data, ROLE_USER)
|
||||
}
|
||||
|
||||
pub fn verify_token_admin(bearer: Option<BearerAuth>, secret_key: &[u8]) -> bool {
|
||||
pub fn verify_token_admin(bearer: &str, secret_key: &[u8]) -> bool {
|
||||
has_role(verify_token(bearer, secret_key), ROLE_ADMIN)
|
||||
}
|
||||
|
||||
pub fn verify_token_user(bearer: Option<BearerAuth>, secret_key: &[u8]) -> bool {
|
||||
pub fn verify_token_user(bearer: &str, secret_key: &[u8]) -> bool {
|
||||
has_role(verify_token(bearer, secret_key), ROLE_USER)
|
||||
}
|
||||
|
||||
fn validate_request(
|
||||
req: ServiceRequest,
|
||||
credentials: Option<BearerAuth>,
|
||||
verify_fn: fn(Option<BearerAuth>, &[u8]) -> bool, // Funktions-Parameter für Admin/User-Check
|
||||
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||
if let Some(app_state) = req.app_data::<web::Data<Arc<AppState>>>() {
|
||||
if let Some(web_auth_config) = app_state.config.web_auth.as_ref() {
|
||||
let secret_key = web_auth_config.secret.as_ref();
|
||||
if verify_fn(credentials, secret_key) {
|
||||
return Ok(req);
|
||||
}
|
||||
app_state: &Arc<AppState>,
|
||||
token: &str,
|
||||
verify_fn: fn(&str, &[u8]) -> bool,
|
||||
) -> Result<(), ()> {
|
||||
if let Some(web_auth_config) = app_state.config.web_auth.as_ref() {
|
||||
let secret_key = web_auth_config.secret.as_ref();
|
||||
if verify_fn(token, secret_key) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err((actix_web::error::ErrorUnauthorized("Unauthorized"), req))
|
||||
Err(())
|
||||
}
|
||||
|
||||
#[debug_middleware]
|
||||
pub async fn validator_admin(
|
||||
req: ServiceRequest,
|
||||
credentials: Option<BearerAuth>,
|
||||
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||
validate_request(req, credentials, verify_token_admin)
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum_auth::AuthBearer(token): axum_auth::AuthBearer,
|
||||
request: axum::extract::Request,
|
||||
next: axum::middleware::Next,
|
||||
) -> Result<axum::response::Response, axum::http::StatusCode> {
|
||||
match validate_request(&app_state, &token, verify_token_admin) {
|
||||
Ok(()) => Ok(next.run(request).await),
|
||||
Err(()) => Err(axum::http::StatusCode::UNAUTHORIZED)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_middleware]
|
||||
pub async fn validator_user(
|
||||
req: ServiceRequest,
|
||||
credentials: Option<BearerAuth>,
|
||||
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||
validate_request(req, credentials, verify_token_user)
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum_auth::AuthBearer(token): axum_auth::AuthBearer,
|
||||
request: axum::extract::Request,
|
||||
next: axum::middleware::Next,
|
||||
) -> Result<axum::response::Response, axum::http::StatusCode> {
|
||||
match validate_request(&app_state, &token, verify_token_user) {
|
||||
Ok(()) => Ok(next.run(request).await),
|
||||
Err(()) => Err(axum::http::StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn handle_unauthorized<B>(srvres: ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
// let (req, _) = srvres.into_parts();
|
||||
// let resp = HttpResponse::TemporaryRedirect().insert_header(("Location", "/auth/login")).finish();
|
||||
// let result = ServiceResponse::new(req, resp)
|
||||
// .map_into_boxed_body()
|
||||
// .map_into_right_body();
|
||||
// Ok(ErrorHandlerResponse::Response(result))
|
||||
// }
|
||||
|
||||
+20
-27
@@ -1,9 +1,6 @@
|
||||
#![allow(clippy::empty_docs)]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use enum_iterator::all;
|
||||
use log::{debug, error, log_enabled, trace, Level};
|
||||
use pest::iterators::Pair;
|
||||
@@ -16,21 +13,21 @@ use crate::tools::directed_graph::DirectedGraph;
|
||||
use crate::m3u_filter_error::{create_m3u_filter_error_result, info_err};
|
||||
use crate::utils::sys_utils::exit;
|
||||
|
||||
pub fn get_field_value(pli: &PlaylistItem, field: &ItemField) -> Rc<String> {
|
||||
let header = pli.header.borrow();
|
||||
pub fn get_field_value(pli: &PlaylistItem, field: &ItemField) -> String {
|
||||
let header = &pli.header;
|
||||
let value = match field {
|
||||
ItemField::Group => &header.group,
|
||||
ItemField::Name => &header.name,
|
||||
ItemField::Title => &header.title,
|
||||
ItemField::Url => &header.url,
|
||||
ItemField::Input => &header.input_name,
|
||||
ItemField::Type => &Rc::new(header.item_type.to_string()),
|
||||
ItemField::Group => header.group.to_string(),
|
||||
ItemField::Name => header.name.to_string(),
|
||||
ItemField::Title => header.title.to_string(),
|
||||
ItemField::Url => header.url.to_string(),
|
||||
ItemField::Input => header.input_name.to_string(),
|
||||
ItemField::Type => header.item_type.to_string(),
|
||||
};
|
||||
Rc::clone(value)
|
||||
value.to_string()
|
||||
}
|
||||
|
||||
pub fn set_field_value(pli: &PlaylistItem, field: &ItemField, value: Rc<String>) {
|
||||
let header = &mut pli.header.borrow_mut();
|
||||
pub fn set_field_value(pli: &mut PlaylistItem, field: &ItemField, value: String) {
|
||||
let header = &mut pli.header;
|
||||
match field {
|
||||
ItemField::Group => header.group = value,
|
||||
ItemField::Name => header.name = value,
|
||||
@@ -42,13 +39,12 @@ pub fn set_field_value(pli: &PlaylistItem, field: &ItemField, value: Rc<String>)
|
||||
}
|
||||
|
||||
pub struct ValueProvider<'a> {
|
||||
pub pli: RefCell<&'a PlaylistItem>,
|
||||
pub pli: &'a PlaylistItem,
|
||||
}
|
||||
|
||||
impl ValueProvider<'_> {
|
||||
fn call(&self, field: &ItemField) -> Rc<String> {
|
||||
let pli = *self.pli.borrow();
|
||||
get_field_value(pli, field)
|
||||
fn call(&self, field: &ItemField) -> String {
|
||||
get_field_value(self.pli, field)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,9 +548,6 @@ pub fn apply_templates_to_pattern(pattern: &str, templates: &Vec<PatternTemplate
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use crate::foundation::filter::{get_filter, MockValueProcessor, ValueProvider};
|
||||
@@ -562,11 +555,11 @@ mod tests {
|
||||
|
||||
fn create_mock_pli(name: &str, group: &str) -> PlaylistItem {
|
||||
PlaylistItem {
|
||||
header: RefCell::new(PlaylistItemHeader {
|
||||
name: Rc::new(name.to_string()),
|
||||
group: Rc::new(group.to_string()),
|
||||
header: PlaylistItemHeader {
|
||||
name: name.to_string(),
|
||||
group: group.to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +618,7 @@ mod tests {
|
||||
.iter()
|
||||
.filter(|&chan| {
|
||||
let provider = ValueProvider {
|
||||
pli: RefCell::new(chan),
|
||||
pli: chan,
|
||||
};
|
||||
filter.filter(&provider, &mut processor)
|
||||
})
|
||||
@@ -681,7 +674,7 @@ mod tests {
|
||||
.iter()
|
||||
.filter(|&chan| {
|
||||
let provider = ValueProvider {
|
||||
pli: RefCell::new(chan),
|
||||
pli: chan,
|
||||
};
|
||||
filter.filter(&provider, &mut processor)
|
||||
})
|
||||
|
||||
+18
-16
@@ -9,7 +9,6 @@ mod modules;
|
||||
|
||||
include_modules!();
|
||||
|
||||
use actix_rt::System;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -155,18 +154,21 @@ fn main() {
|
||||
info!("Freeze frame video loaded from {:?}", cfg.channel_unavailable_file.as_ref().map_or("?", |v| v.as_str()));
|
||||
}
|
||||
|
||||
if args.server {
|
||||
match config_reader::read_api_proxy_config(args.api_proxy, &mut cfg) {
|
||||
Ok(Some(api_proxy_file)) => {
|
||||
info!("Api Proxy File: {api_proxy_file:?}");
|
||||
},
|
||||
Ok(None) => {}
|
||||
Err(err) => exit!("{err}"),
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let () = rt.block_on(async {
|
||||
if args.server {
|
||||
match config_reader::read_api_proxy_config(args.api_proxy, &mut cfg).await {
|
||||
Ok(Some(api_proxy_file)) => {
|
||||
info!("Api Proxy File: {api_proxy_file:?}");
|
||||
},
|
||||
Ok(None) => {}
|
||||
Err(err) => exit!("{err}"),
|
||||
}
|
||||
start_in_server_mode(Arc::new(cfg), Arc::new(targets)).await;
|
||||
} else {
|
||||
start_in_cli_mode(Arc::new(cfg), Arc::new(targets)).await;
|
||||
}
|
||||
start_in_server_mode(Arc::new(cfg), Arc::new(targets));
|
||||
} else {
|
||||
start_in_cli_mode(Arc::new(cfg), Arc::new(targets));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn create_directories(cfg: &Config, temp_path: &Path) {
|
||||
@@ -198,13 +200,13 @@ fn create_directories(cfg: &Config, temp_path: &Path) {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_in_cli_mode(cfg: Arc<Config>, targets: Arc<ProcessTargets>) {
|
||||
async fn start_in_cli_mode(cfg: Arc<Config>, targets: Arc<ProcessTargets>) {
|
||||
let client = Arc::new(reqwest::Client::new());
|
||||
System::new().block_on(async { playlist::exec_processing(client, cfg, targets).await });
|
||||
playlist::exec_processing(client, cfg, targets).await;
|
||||
}
|
||||
|
||||
fn start_in_server_mode(cfg: Arc<Config>, targets: Arc<ProcessTargets>) {
|
||||
if let Err(err) = api::main_api::start_server(cfg, targets) {
|
||||
async fn start_in_server_mode(cfg: Arc<Config>, targets: Arc<ProcessTargets>) {
|
||||
if let Err(err) = api::main_api::start_server(cfg, targets).await {
|
||||
exit!("Can't start server: {err}");
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ fn send_http_post_request(msg: &str, messaging: &MessagingConfig) {
|
||||
if let Some(rest) = &messaging.rest {
|
||||
let url = rest.url.clone();
|
||||
let data = msg.to_owned();
|
||||
actix_rt::spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
let client = reqwest::Client::new();
|
||||
match client
|
||||
.post(&url)
|
||||
@@ -59,7 +59,7 @@ fn send_pushover_message(msg: &str, messaging: &MessagingConfig) {
|
||||
.append_pair("message", msg)
|
||||
.finish();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
let client = reqwest::Client::new();
|
||||
match client
|
||||
.post(url)
|
||||
|
||||
@@ -172,7 +172,7 @@ impl ProxyUserCredentials {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_permissions(&self, app_state: &AppState) -> bool {
|
||||
pub async fn has_permissions(&self, app_state: &AppState) -> bool {
|
||||
if app_state.config.user_access_control {
|
||||
if let Some(exp_date) = self.exp_date.as_ref() {
|
||||
let now = Local::now();
|
||||
@@ -182,7 +182,7 @@ impl ProxyUserCredentials {
|
||||
}
|
||||
}
|
||||
if let Some(max_connections) = self.max_connections.as_ref() {
|
||||
if *max_connections < app_state.get_active_connections_for_user(&self.username) {
|
||||
if *max_connections < app_state.get_active_connections_for_user(&self.username).await {
|
||||
debug!("User access denied, too many connections: {}", self.username);
|
||||
return false;
|
||||
}
|
||||
|
||||
+21
-20
@@ -1,6 +1,5 @@
|
||||
#![allow(clippy::struct_excessive_bools)]
|
||||
use enum_iterator::Sequence;
|
||||
use parking_lot::RwLock;
|
||||
use std::borrow::BorrowMut;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
@@ -8,7 +7,8 @@ use std::fs::File;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::auth::user::UserCredential;
|
||||
use log::{debug, error, warn};
|
||||
@@ -1197,14 +1197,14 @@ pub struct Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn set_api_proxy(&mut self, api_proxy: Option<ApiProxyConfig>) -> Result<(), M3uFilterError> {
|
||||
pub async fn set_api_proxy(&mut self, api_proxy: Option<ApiProxyConfig>) -> Result<(), M3uFilterError> {
|
||||
self.t_api_proxy = Arc::new(RwLock::new(api_proxy));
|
||||
self.check_target_user()
|
||||
self.check_target_user().await
|
||||
}
|
||||
|
||||
fn check_username(&self, output_username: Option<&str>, target_name: &str) -> Result<(), M3uFilterError> {
|
||||
async fn check_username(&self, output_username: Option<&str>, target_name: &str) -> Result<(), M3uFilterError> {
|
||||
if let Some(username) = output_username {
|
||||
if let Some((_, config_target)) = self.get_target_for_username(username) {
|
||||
if let Some((_, config_target)) = self.get_target_for_username(username).await {
|
||||
if config_target.name != target_name {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "User:{username} does not belong to target: {}", target_name);
|
||||
}
|
||||
@@ -1214,7 +1214,7 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn check_target_user(&mut self) -> Result<(), M3uFilterError> {
|
||||
async fn check_target_user(&mut self) -> Result<(), M3uFilterError> {
|
||||
let check_homerun = self.hdhomerun.as_ref().is_some_and(|h| h.enabled);
|
||||
for source in &self.sources {
|
||||
for target in &source.targets {
|
||||
@@ -1222,12 +1222,12 @@ impl Config {
|
||||
match output {
|
||||
TargetOutput::Xtream(_) | TargetOutput::M3u(_) => {}
|
||||
TargetOutput::Strm(strm_output) => {
|
||||
self.check_username(strm_output.username.as_deref(), &target.name)?;
|
||||
self.check_username(strm_output.username.as_deref(), &target.name).await?;
|
||||
}
|
||||
TargetOutput::HdHomeRun(hdhomerun_output) => {
|
||||
if check_homerun {
|
||||
let hdhr_name = &hdhomerun_output.device;
|
||||
self.check_username(Some(&hdhomerun_output.username), &target.name)?;
|
||||
self.check_username(Some(&hdhomerun_output.username), &target.name).await?;
|
||||
if let Some(homerun) = &mut self.hdhomerun {
|
||||
for device in &mut homerun.devices {
|
||||
if &device.name == hdhr_name {
|
||||
@@ -1282,23 +1282,24 @@ impl Config {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_target_for_username(&self, username: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
if let Some(credentials) = self.get_user_credentials(username) {
|
||||
return self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(&credentials.username, &credentials.password)));
|
||||
pub async fn get_target_for_username(&self, username: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
if let Some(credentials) = self.get_user_credentials(username).await {
|
||||
return self.t_api_proxy.read().await.as_ref()
|
||||
.and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(&credentials.username, &credentials.password)));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(username, password)))
|
||||
pub async fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
self.t_api_proxy.read().await.as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(username, password)))
|
||||
}
|
||||
|
||||
pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name_by_token(token)))
|
||||
pub async fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
self.t_api_proxy.read().await.as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name_by_token(token)))
|
||||
}
|
||||
|
||||
pub fn get_user_credentials(&self, username: &str) -> Option<ProxyUserCredentials> {
|
||||
self.t_api_proxy.read().as_ref().and_then(|api_proxy| api_proxy.get_user_credentials(username))
|
||||
pub async fn get_user_credentials(&self, username: &str) -> Option<ProxyUserCredentials> {
|
||||
self.t_api_proxy.read().await.as_ref().and_then(|api_proxy| api_proxy.get_user_credentials(username))
|
||||
}
|
||||
|
||||
pub fn get_input_by_name(&self, input_name: &str) -> Option<&ConfigInput> {
|
||||
@@ -1526,8 +1527,8 @@ impl Config {
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if default server invalid
|
||||
pub fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo {
|
||||
let server_info_list = self.t_api_proxy.read().as_ref().unwrap().server.clone();
|
||||
pub async fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo {
|
||||
let server_info_list = self.t_api_proxy.read().await.as_ref().unwrap().server.clone();
|
||||
let server_info_name = user.server.as_ref().map_or("default", |server_name| server_name.as_str());
|
||||
server_info_list.iter().find(|c| c.name.eq(server_info_name)).map_or_else(|| server_info_list.first().unwrap().clone(), Clone::clone)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Healthcheck {
|
||||
pub status: String,
|
||||
pub version: String,
|
||||
|
||||
+11
-13
@@ -2,10 +2,8 @@ use enum_iterator::Sequence;
|
||||
use log::{debug, error, trace};
|
||||
use regex::{Regex};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::Arc;
|
||||
@@ -270,23 +268,23 @@ impl Mapper {
|
||||
}
|
||||
|
||||
pub struct MappingValueProcessor<'a> {
|
||||
pub pli: RefCell<&'a PlaylistItem>,
|
||||
pub pli: &'a mut PlaylistItem,
|
||||
pub mapper: &'a Mapper,
|
||||
}
|
||||
|
||||
impl MappingValueProcessor<'_> {
|
||||
fn get_property(&self, key: &str) -> Option<Rc<String>> {
|
||||
self.pli.borrow().header.borrow().get_field(key)
|
||||
fn get_property(&self, key: &str) -> Option<String> {
|
||||
self.pli.header.get_field(key)
|
||||
}
|
||||
|
||||
fn set_property(&self, key: &str, value: &str) {
|
||||
if !self.pli.borrow().header.borrow_mut().set_field(key, value) {
|
||||
fn set_property(&mut self, key: &str, value: &str) {
|
||||
if !self.pli.header.set_field(key, value) {
|
||||
error!("Cant set unknown field {} to {}", key, value);
|
||||
}
|
||||
trace!("Property {} set to {}", key, value);
|
||||
}
|
||||
|
||||
fn apply_attributes(&self, captured_names: &HashMap<&str, &str>) {
|
||||
fn apply_attributes(&mut self, captured_names: &HashMap<&str, &str>) {
|
||||
let mapper = self.mapper;
|
||||
let attr_re = &mapper.t_attre.as_ref().unwrap();
|
||||
let attributes = &mapper.attributes;
|
||||
@@ -303,7 +301,7 @@ impl MappingValueProcessor<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_tags(&self, value: &str, captures: &HashMap<&str, &str>) -> Option<String> {
|
||||
fn apply_tags(&mut self, value: &str, captures: &HashMap<&str, &str>) -> Option<String> {
|
||||
let mut new_value = String::from(value);
|
||||
let tag_captures = self.mapper.t_tagre.as_ref().unwrap().captures_iter(value)
|
||||
.filter(|caps| caps.len() > 1)
|
||||
@@ -342,7 +340,7 @@ impl MappingValueProcessor<'_> {
|
||||
Some(new_value)
|
||||
}
|
||||
|
||||
fn apply_suffix(&self, captures: &HashMap<&str, &str>) {
|
||||
fn apply_suffix(&mut self, captures: &HashMap<&str, &str>) {
|
||||
let mapper = self.mapper;
|
||||
let suffix = &mapper.suffix;
|
||||
|
||||
@@ -356,7 +354,7 @@ impl MappingValueProcessor<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_prefix(&self, captures: &HashMap<&str, &str>) {
|
||||
fn apply_prefix(&mut self, captures: &HashMap<&str, &str>) {
|
||||
let mapper = self.mapper;
|
||||
let prefix = &mapper.prefix;
|
||||
for (key, value) in prefix {
|
||||
@@ -369,7 +367,7 @@ impl MappingValueProcessor<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_assignments(&self) {
|
||||
fn apply_assignments(&mut self) {
|
||||
let mapper = self.mapper;
|
||||
let assignments = &mapper.assignments;
|
||||
for (key, value) in assignments {
|
||||
@@ -387,7 +385,7 @@ impl MappingValueProcessor<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transform(&self) {
|
||||
fn apply_transform(&mut self) {
|
||||
let mapper = self.mapper;
|
||||
match &mapper.transform {
|
||||
None => {}
|
||||
|
||||
+94
-95
@@ -1,8 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::PartialEq;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::model::api_proxy::ProxyUserCredentials;
|
||||
use crate::model::config::{ConfigInput, ConfigTargetOptions};
|
||||
use crate::model::xmltv::TVGuide;
|
||||
@@ -15,11 +12,11 @@ use crate::utils::network::request::extract_extension_from_url;
|
||||
// https://de.wikipedia.org/wiki/M3U
|
||||
// https://siptv.eu/howto/playlist.html
|
||||
|
||||
pub trait PlaylistEntry {
|
||||
pub trait PlaylistEntry: Send + Sync {
|
||||
fn get_virtual_id(&self) -> u32;
|
||||
fn get_provider_id(&self) -> Option<u32>;
|
||||
fn get_category_id(&self) -> Option<u32>;
|
||||
fn get_provider_url(&self) -> Rc<String>;
|
||||
fn get_provider_url(&self) -> String;
|
||||
fn get_uuid(&self) -> UUIDType;
|
||||
}
|
||||
|
||||
@@ -129,7 +126,7 @@ impl Display for PlaylistItemType {
|
||||
}
|
||||
|
||||
pub trait FieldGetAccessor {
|
||||
fn get_field(&self, field: &str) -> Option<Rc<String>>;
|
||||
fn get_field(&self, field: &str) -> Option<String>;
|
||||
}
|
||||
pub trait FieldSetAccessor {
|
||||
fn set_field(&mut self, field: &str, value: &str) -> bool;
|
||||
@@ -139,35 +136,35 @@ pub type UUIDType = [u8; 32];
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PlaylistItemHeader {
|
||||
pub uuid: Rc<UUIDType>, // calculated
|
||||
pub id: Rc<String>, // provider id
|
||||
pub uuid: UUIDType, // calculated
|
||||
pub id: String, // provider id
|
||||
pub virtual_id: u32, // virtual id
|
||||
pub name: Rc<String>,
|
||||
pub chno: Rc<String>,
|
||||
pub logo: Rc<String>,
|
||||
pub logo_small: Rc<String>,
|
||||
pub group: Rc<String>,
|
||||
pub title: Rc<String>,
|
||||
pub parent_code: Rc<String>,
|
||||
pub audio_track: Rc<String>,
|
||||
pub time_shift: Rc<String>,
|
||||
pub rec: Rc<String>,
|
||||
pub url: Rc<String>,
|
||||
pub epg_channel_id: Option<Rc<String>>,
|
||||
pub name: String,
|
||||
pub chno: String,
|
||||
pub logo: String,
|
||||
pub logo_small: String,
|
||||
pub group: String,
|
||||
pub title: String,
|
||||
pub parent_code: String,
|
||||
pub audio_track: String,
|
||||
pub time_shift: String,
|
||||
pub rec: String,
|
||||
pub url: String,
|
||||
pub epg_channel_id: Option<String>,
|
||||
pub xtream_cluster: XtreamCluster,
|
||||
pub additional_properties: Option<Value>,
|
||||
#[serde(default, skip_serializing, skip_deserializing)]
|
||||
pub item_type: PlaylistItemType,
|
||||
#[serde(default)]
|
||||
pub category_id: u32,
|
||||
pub input_name: Rc<String>,
|
||||
pub input_name: String,
|
||||
}
|
||||
|
||||
impl PlaylistItemHeader {
|
||||
pub fn gen_uuid(&mut self) {
|
||||
self.uuid = Rc::new(generate_playlist_uuid(&self.input_name, &self.id, self.item_type, &self.url));
|
||||
self.uuid = generate_playlist_uuid(&self.input_name, &self.id, self.item_type, &self.url);
|
||||
}
|
||||
pub const fn get_uuid(&self) -> &Rc<UUIDType> {
|
||||
pub const fn get_uuid(&self) -> &UUIDType {
|
||||
&self.uuid
|
||||
}
|
||||
|
||||
@@ -175,7 +172,7 @@ impl PlaylistItemHeader {
|
||||
match get_provider_id(&self.id, &self.url) {
|
||||
None => None,
|
||||
Some(newid) => {
|
||||
self.id = Rc::new(newid.to_string());
|
||||
self.id = newid.to_string();
|
||||
Some(newid)
|
||||
}
|
||||
}
|
||||
@@ -233,7 +230,7 @@ macro_rules! to_m3u_resource_non_empty_fields {
|
||||
macro_rules! generate_field_accessor_impl_for_playlist_item_header {
|
||||
($($prop:ident),*;) => {
|
||||
impl FieldGetAccessor for PlaylistItemHeader {
|
||||
fn get_field(&self, field: &str) -> Option<Rc<String>> {
|
||||
fn get_field(&self, field: &str) -> Option<String> {
|
||||
match field {
|
||||
$(
|
||||
stringify!($prop) => Some(self.$prop.clone()),
|
||||
@@ -249,12 +246,12 @@ macro_rules! generate_field_accessor_impl_for_playlist_item_header {
|
||||
match field {
|
||||
$(
|
||||
stringify!($prop) => {
|
||||
self.$prop = Rc::new(val);
|
||||
self.$prop = val;
|
||||
true
|
||||
}
|
||||
)*
|
||||
"epg_channel_id" | "epg_id" => {
|
||||
self.epg_channel_id = Some(Rc::new(value.to_owned()));
|
||||
self.epg_channel_id = Some(value.to_owned());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
@@ -269,20 +266,20 @@ generate_field_accessor_impl_for_playlist_item_header!(id, /*virtual_id,*/ name,
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct M3uPlaylistItem {
|
||||
pub virtual_id: u32,
|
||||
pub provider_id: Rc<String>,
|
||||
pub name: Rc<String>,
|
||||
pub chno: Rc<String>,
|
||||
pub logo: Rc<String>,
|
||||
pub logo_small: Rc<String>,
|
||||
pub group: Rc<String>,
|
||||
pub title: Rc<String>,
|
||||
pub parent_code: Rc<String>,
|
||||
pub audio_track: Rc<String>,
|
||||
pub time_shift: Rc<String>,
|
||||
pub rec: Rc<String>,
|
||||
pub url: Rc<String>,
|
||||
pub epg_channel_id: Option<Rc<String>>,
|
||||
pub input_name: Rc<String>,
|
||||
pub provider_id: String,
|
||||
pub name: String,
|
||||
pub chno: String,
|
||||
pub logo: String,
|
||||
pub logo_small: String,
|
||||
pub group: String,
|
||||
pub title: String,
|
||||
pub parent_code: String,
|
||||
pub audio_track: String,
|
||||
pub time_shift: String,
|
||||
pub rec: String,
|
||||
pub url: String,
|
||||
pub epg_channel_id: Option<String>,
|
||||
pub input_name: String,
|
||||
pub item_type: PlaylistItemType,
|
||||
#[serde(skip)]
|
||||
pub t_stream_url: String,
|
||||
@@ -333,8 +330,8 @@ impl PlaylistEntry for M3uPlaylistItem {
|
||||
None
|
||||
}
|
||||
#[inline]
|
||||
fn get_provider_url(&self) -> Rc<String> {
|
||||
Rc::clone(&self.url)
|
||||
fn get_provider_url(&self) -> String {
|
||||
self.url.to_string()
|
||||
}
|
||||
|
||||
fn get_uuid(&self) -> UUIDType {
|
||||
@@ -345,7 +342,7 @@ impl PlaylistEntry for M3uPlaylistItem {
|
||||
macro_rules! generate_field_accessor_impl_for_m3u_playlist_item {
|
||||
($($prop:ident),*;) => {
|
||||
impl FieldGetAccessor for M3uPlaylistItem {
|
||||
fn get_field(&self, field: &str) -> Option<Rc<String>> {
|
||||
fn get_field(&self, field: &str) -> Option<String> {
|
||||
match field {
|
||||
$(
|
||||
stringify!($prop) => Some(self.$prop.clone()),
|
||||
@@ -364,20 +361,20 @@ generate_field_accessor_impl_for_m3u_playlist_item!(provider_id, name, chno, log
|
||||
pub struct XtreamPlaylistItem {
|
||||
pub virtual_id: u32,
|
||||
pub provider_id: u32,
|
||||
pub name: Rc<String>,
|
||||
pub logo: Rc<String>,
|
||||
pub logo_small: Rc<String>,
|
||||
pub group: Rc<String>,
|
||||
pub title: Rc<String>,
|
||||
pub parent_code: Rc<String>,
|
||||
pub rec: Rc<String>,
|
||||
pub url: Rc<String>,
|
||||
pub epg_channel_id: Option<Rc<String>>,
|
||||
pub name: String,
|
||||
pub logo: String,
|
||||
pub logo_small: String,
|
||||
pub group: String,
|
||||
pub title: String,
|
||||
pub parent_code: String,
|
||||
pub rec: String,
|
||||
pub url: String,
|
||||
pub epg_channel_id: Option<String>,
|
||||
pub xtream_cluster: XtreamCluster,
|
||||
pub additional_properties: Option<String>,
|
||||
pub item_type: PlaylistItemType,
|
||||
pub category_id: u32,
|
||||
pub input_name: Rc<String>,
|
||||
pub input_name: String,
|
||||
pub channel_no: u32,
|
||||
}
|
||||
|
||||
@@ -410,8 +407,8 @@ impl PlaylistEntry for XtreamPlaylistItem {
|
||||
None
|
||||
}
|
||||
#[inline]
|
||||
fn get_provider_url(&self) -> Rc<String> {
|
||||
Rc::clone(&self.url)
|
||||
fn get_provider_url(&self) -> String {
|
||||
self.url.to_string()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -420,22 +417,22 @@ impl PlaylistEntry for XtreamPlaylistItem {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option<Rc<String>> {
|
||||
pub fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option<String> {
|
||||
match value {
|
||||
Some(Value::String(url)) => Some(Rc::new(url.clone())),
|
||||
Some(Value::String(url)) => Some(url.clone()),
|
||||
Some(Value::Array(values)) => {
|
||||
match values.as_slice() {
|
||||
[Value::String(single)] => Some(Rc::new(single.to_string())),
|
||||
[Value::String(single)] => Some(single.to_string()),
|
||||
multiple if !multiple.is_empty() => {
|
||||
if let Some(index) = field.rfind('_') {
|
||||
if let Ok(bd_index) = field[index + 1..].parse::<usize>() {
|
||||
if let Some(Value::String(selected)) = multiple.get(bd_index) {
|
||||
return Some(Rc::new(selected.to_string()));
|
||||
return Some(selected.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Value::String(url) = &multiple[0] {
|
||||
Some(Rc::new(url.to_string()))
|
||||
Some(url.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -450,7 +447,7 @@ pub fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option<Rc<
|
||||
macro_rules! generate_field_accessor_impl_for_xtream_playlist_item {
|
||||
($($prop:ident),*;) => {
|
||||
impl FieldGetAccessor for XtreamPlaylistItem {
|
||||
fn get_field(&self, field: &str) -> Option<Rc<String>> {
|
||||
fn get_field(&self, field: &str) -> Option<String> {
|
||||
match field {
|
||||
$(
|
||||
stringify!($prop) => Some(self.$prop.clone()),
|
||||
@@ -462,7 +459,7 @@ macro_rules! generate_field_accessor_impl_for_xtream_playlist_item {
|
||||
return match props {
|
||||
Some(doc) => {
|
||||
return if field == PROP_COVER {
|
||||
doc.get(field).and_then(|value| value.as_str().map(|s| Rc::new(s.to_string())))
|
||||
doc.get(field).and_then(|value| value.as_str().map(|s| s.to_string()))
|
||||
} else {
|
||||
get_backdrop_path_value(field, doc.get(PROP_BACKDROP_PATH))
|
||||
}
|
||||
@@ -480,30 +477,30 @@ macro_rules! generate_field_accessor_impl_for_xtream_playlist_item {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlaylistItem {
|
||||
pub header: RefCell<PlaylistItemHeader>,
|
||||
pub header: PlaylistItemHeader,
|
||||
}
|
||||
|
||||
generate_field_accessor_impl_for_xtream_playlist_item!(name, logo, logo_small, group, title, parent_code, rec, url;);
|
||||
|
||||
impl PlaylistItem {
|
||||
pub fn to_m3u(&self) -> M3uPlaylistItem {
|
||||
let header = self.header.borrow();
|
||||
let header = &self.header;
|
||||
M3uPlaylistItem {
|
||||
virtual_id: header.virtual_id,
|
||||
provider_id: Rc::clone(&header.id),
|
||||
name: Rc::clone(if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }),
|
||||
chno: Rc::clone(&header.chno),
|
||||
logo: Rc::clone(&header.logo),
|
||||
logo_small: Rc::clone(&header.logo_small),
|
||||
group: Rc::clone(&header.group),
|
||||
title: Rc::clone(&header.title),
|
||||
parent_code: Rc::clone(&header.parent_code),
|
||||
audio_track: Rc::clone(&header.audio_track),
|
||||
time_shift: Rc::clone(&header.time_shift),
|
||||
rec: Rc::clone(&header.rec),
|
||||
url: Rc::clone(&header.url),
|
||||
provider_id: header.id.to_string(),
|
||||
name: if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }.to_string(),
|
||||
chno: header.chno.to_string(),
|
||||
logo: header.logo.to_string(),
|
||||
logo_small: header.logo_small.to_string(),
|
||||
group: header.group.to_string(),
|
||||
title: header.title.to_string(),
|
||||
parent_code: header.parent_code.to_string(),
|
||||
audio_track: header.audio_track.to_string(),
|
||||
time_shift: header.time_shift.to_string(),
|
||||
rec: header.rec.to_string(),
|
||||
url: header.url.to_string(),
|
||||
epg_channel_id: header.epg_channel_id.clone(),
|
||||
input_name: Rc::clone(&header.input_name),
|
||||
input_name: header.input_name.to_string(),
|
||||
item_type: header.item_type,
|
||||
t_stream_url: header.url.to_string(),
|
||||
t_resource_url: None,
|
||||
@@ -511,7 +508,7 @@ impl PlaylistItem {
|
||||
}
|
||||
|
||||
pub fn to_xtream(&self) -> XtreamPlaylistItem {
|
||||
let header = self.header.borrow();
|
||||
let header = &self.header;
|
||||
let provider_id = header.id.parse::<u32>().unwrap_or_default();
|
||||
let mut additional_properties = None;
|
||||
if header.xtream_cluster != XtreamCluster::Live {
|
||||
@@ -549,20 +546,20 @@ impl PlaylistItem {
|
||||
XtreamPlaylistItem {
|
||||
virtual_id: header.virtual_id,
|
||||
provider_id,
|
||||
name: Rc::clone(if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }),
|
||||
logo: Rc::clone(&header.logo),
|
||||
logo_small: Rc::clone(&header.logo_small),
|
||||
group: Rc::clone(&header.group),
|
||||
title: Rc::clone(&header.title),
|
||||
parent_code: Rc::clone(&header.parent_code),
|
||||
rec: Rc::clone(&header.rec),
|
||||
url: Rc::clone(&header.url),
|
||||
name: if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }.to_string(),
|
||||
logo: header.logo.to_string(),
|
||||
logo_small: header.logo_small.to_string(),
|
||||
group: header.group.to_string(),
|
||||
title: header.title.to_string(),
|
||||
parent_code: header.parent_code.to_string(),
|
||||
rec: header.rec.to_string(),
|
||||
url: header.url.to_string(),
|
||||
epg_channel_id: header.epg_channel_id.clone(),
|
||||
xtream_cluster: header.xtream_cluster,
|
||||
additional_properties,
|
||||
item_type: header.item_type,
|
||||
category_id: header.category_id,
|
||||
input_name: Rc::clone(&header.input_name),
|
||||
input_name: header.input_name.to_string(),
|
||||
channel_no: header.chno.parse::<u32>().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
@@ -571,11 +568,11 @@ impl PlaylistItem {
|
||||
impl PlaylistEntry for PlaylistItem {
|
||||
#[inline]
|
||||
fn get_virtual_id(&self) -> u32 {
|
||||
self.header.borrow().virtual_id
|
||||
self.header.virtual_id
|
||||
}
|
||||
|
||||
fn get_provider_id(&self) -> Option<u32> {
|
||||
let header = self.header.borrow();
|
||||
let header = &self.header;
|
||||
get_provider_id(&header.id, &header.url)
|
||||
}
|
||||
|
||||
@@ -585,12 +582,12 @@ impl PlaylistEntry for PlaylistItem {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_provider_url(&self) -> Rc<String> {
|
||||
Rc::clone(&self.header.borrow().url)
|
||||
fn get_provider_url(&self) -> String {
|
||||
self.header.url.to_string()
|
||||
}
|
||||
#[inline]
|
||||
fn get_uuid(&self) -> UUIDType {
|
||||
let header = self.header.borrow();
|
||||
let header = &self.header;
|
||||
generate_playlist_uuid(&header.input_name, &header.id, header.item_type, &header.url)
|
||||
}
|
||||
}
|
||||
@@ -598,7 +595,7 @@ impl PlaylistEntry for PlaylistItem {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlaylistGroup {
|
||||
pub id: u32,
|
||||
pub title: Rc<String>,
|
||||
pub title: String,
|
||||
pub channels: Vec<PlaylistItem>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub xtream_cluster: XtreamCluster,
|
||||
@@ -607,7 +604,9 @@ pub struct PlaylistGroup {
|
||||
impl PlaylistGroup {
|
||||
#[inline]
|
||||
pub fn on_load(&mut self) {
|
||||
self.channels.iter().for_each(|pl| pl.header.borrow_mut().gen_uuid());
|
||||
for pl in &mut self.channels {
|
||||
pl.header.gen_uuid();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
+4
-5
@@ -1,7 +1,6 @@
|
||||
use std::collections::{HashMap};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use std::sync::Arc;
|
||||
use quick_xml::{Error, Writer};
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||
|
||||
@@ -17,8 +16,8 @@ pub const EPG_ATTRIB_CHANNEL: &str = "channel";
|
||||
pub struct XmlTag {
|
||||
pub name: String,
|
||||
pub value: Option<String>,
|
||||
pub attributes: Option<Rc<HashMap<String, String>>>,
|
||||
pub children: Option<Vec<Rc<XmlTag>>>,
|
||||
pub attributes: Option<Arc<HashMap<String, String>>>,
|
||||
pub children: Option<Vec<Arc<XmlTag>>>,
|
||||
}
|
||||
|
||||
impl XmlTag {
|
||||
@@ -45,7 +44,7 @@ impl XmlTag {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Epg {
|
||||
pub attributes: Option<Rc<HashMap<String, String>>>,
|
||||
pub attributes: Option<Arc<HashMap<String, String>>>,
|
||||
pub children: Vec<XmlTag>,
|
||||
}
|
||||
|
||||
|
||||
+38
-40
@@ -1,7 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::iter::FromIterator;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::model::api_proxy::{ProxyType, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, XtreamTargetOutput};
|
||||
use crate::model::playlist::{PlaylistEntry, PlaylistItem, XtreamCluster, XtreamPlaylistItem};
|
||||
@@ -79,29 +77,29 @@ fn value_to_string(v: &Value) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_as_option_rc_string<'de, D>(deserializer: D) -> Result<Option<Rc<String>>, D::Error>
|
||||
fn deserialize_as_option_rc_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value: Value = Deserialize::deserialize(deserializer)?;
|
||||
|
||||
match &value {
|
||||
Value::String(s) => Ok(Some(Rc::new(s.to_owned()))),
|
||||
Value::Number(s) => Ok(Some(Rc::new(s.to_string()))),
|
||||
Value::String(s) => Ok(Some(s.to_owned())),
|
||||
Value::Number(s) => Ok(Some(s.to_string())),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_as_rc_string<'de, D>(deserializer: D) -> Result<Rc<String>, D::Error>
|
||||
fn deserialize_as_rc_string<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value: Value = Deserialize::deserialize(deserializer)?;
|
||||
|
||||
match &value {
|
||||
Value::String(s) => Ok(Rc::new(s.to_owned())),
|
||||
Value::Null => Ok(Rc::new(String::new())),
|
||||
_ => Ok(Rc::new(value.to_string())),
|
||||
Value::String(s) => Ok(s.to_string()),
|
||||
Value::Null => Ok(String::new()),
|
||||
_ => Ok(value.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +117,9 @@ where
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct XtreamCategory {
|
||||
#[serde(deserialize_with = "deserialize_as_rc_string")]
|
||||
pub category_id: Rc<String>,
|
||||
pub category_id: String,
|
||||
#[serde(deserialize_with = "deserialize_as_rc_string")]
|
||||
pub category_name: Rc<String>,
|
||||
pub category_name: String,
|
||||
//pub parent_id: i32,
|
||||
#[serde(default)]
|
||||
pub channels: Vec<PlaylistItem>,
|
||||
@@ -136,65 +134,65 @@ impl XtreamCategory {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct XtreamStream {
|
||||
#[serde(default, deserialize_with = "deserialize_as_rc_string")]
|
||||
pub name: Rc<String>,
|
||||
pub name: String,
|
||||
#[serde(default, deserialize_with = "deserialize_as_rc_string")]
|
||||
pub category_id: Rc<String>,
|
||||
pub category_id: String,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
pub stream_id: Option<u32>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
pub series_id: Option<u32>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_rc_string")]
|
||||
pub stream_icon: Rc<String>,
|
||||
pub stream_icon: String,
|
||||
#[serde(default, deserialize_with = "deserialize_as_rc_string")]
|
||||
pub direct_source: Rc<String>,
|
||||
pub direct_source: String,
|
||||
|
||||
// optional attributes
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub custom_sid: Option<Rc<String>>,
|
||||
pub custom_sid: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_string_array")]
|
||||
pub backdrop_path: Option<Vec<String>>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub added: Option<Rc<String>>,
|
||||
pub added: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub cast: Option<Rc<String>>,
|
||||
pub cast: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub container_extension: Option<Rc<String>>,
|
||||
pub container_extension: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub cover: Option<Rc<String>>,
|
||||
pub cover: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub director: Option<Rc<String>>,
|
||||
pub director: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub episode_run_time: Option<Rc<String>>,
|
||||
pub episode_run_time: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub genre: Option<Rc<String>>,
|
||||
pub genre: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub last_modified: Option<Rc<String>>,
|
||||
pub last_modified: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub plot: Option<Rc<String>>,
|
||||
pub plot: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
pub rating: Option<f64>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
pub rating_5based: Option<f64>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub release_date: Option<Rc<String>>,
|
||||
pub release_date: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub stream_type: Option<Rc<String>>,
|
||||
pub stream_type: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub title: Option<Rc<String>>,
|
||||
pub title: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub year: Option<Rc<String>>,
|
||||
pub year: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub trailer: Option<Rc<String>>,
|
||||
pub trailer: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub youtube_trailer: Option<Rc<String>>,
|
||||
pub youtube_trailer: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub epg_channel_id: Option<Rc<String>>,
|
||||
pub epg_channel_id: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
pub tv_archive: Option<i32>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
pub tv_archive_duration: Option<i32>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_rc_string")]
|
||||
pub tmdb: Option<Rc<String>>,
|
||||
pub tmdb: Option<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
pub is_adult: Option<i32>,
|
||||
|
||||
@@ -561,19 +559,19 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, opti
|
||||
let logo_small_url = if pli.logo_small.is_empty() { String::new() } else { format!("{resource_url}/logo_small") };
|
||||
(Some(resource_url), logo_url, logo_small_url)
|
||||
} else {
|
||||
(None, pli.logo.as_ref().clone(), pli.logo_small.as_ref().clone())
|
||||
(None, pli.logo.clone(), pli.logo_small.clone())
|
||||
}
|
||||
}
|
||||
ProxyType::Redirect => {
|
||||
(None, pli.logo.as_ref().clone(), pli.logo_small.as_ref().clone())
|
||||
(None, pli.logo.clone(), pli.logo_small.clone())
|
||||
}
|
||||
};
|
||||
let mut document = serde_json::Map::from_iter([
|
||||
("category_id".to_string(), Value::String(format!("{}", &pli.category_id))),
|
||||
("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from(pli.category_id))]))),
|
||||
("name".to_string(), Value::String(pli.name.as_ref().clone())),
|
||||
("name".to_string(), Value::String(pli.name.clone())),
|
||||
("num".to_string(), Value::Number(serde_json::Number::from(pli.channel_no))),
|
||||
("title".to_string(), Value::String(pli.title.as_ref().clone())),
|
||||
("title".to_string(), Value::String(pli.title.clone())),
|
||||
("stream_icon".to_string(), Value::String(logo)),
|
||||
]);
|
||||
|
||||
@@ -583,18 +581,18 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, opti
|
||||
if options.skip_live_direct_source {
|
||||
document.insert("direct_source".to_string(), Value::String(String::new()));
|
||||
} else {
|
||||
document.insert("direct_source".to_string(), Value::String(pli.url.as_ref().clone()));
|
||||
document.insert("direct_source".to_string(), Value::String(pli.url.clone()));
|
||||
}
|
||||
document.insert("thumbnail".to_string(), Value::String(logo_small));
|
||||
document.insert("custom_sid".to_string(), Value::String(String::new()));
|
||||
document.insert("epg_channel_id".to_string(), pli.epg_channel_id.as_ref().map_or(Value::Null, |epg_id| Value::String(epg_id.as_ref().clone())));
|
||||
document.insert("epg_channel_id".to_string(), pli.epg_channel_id.as_ref().map_or(Value::Null, |epg_id| Value::String(epg_id.clone())));
|
||||
}
|
||||
XtreamCluster::Video => {
|
||||
document.insert("stream_id".to_string(), stream_id_value);
|
||||
if options.skip_video_direct_source {
|
||||
document.insert("direct_source".to_string(), Value::String(String::new()));
|
||||
} else {
|
||||
document.insert("direct_source".to_string(), Value::String(pli.url.as_ref().clone()));
|
||||
document.insert("direct_source".to_string(), Value::String(pli.url.clone()));
|
||||
}
|
||||
document.insert("custom_sid".to_string(), Value::String(String::new()));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::borrow::BorrowMut;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::model::config::{Config, ConfigInput};
|
||||
use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster};
|
||||
use crate::utils::hash_utils::extract_id_from_url;
|
||||
@@ -76,9 +73,9 @@ fn skip_digit(it: &mut std::str::Chars) -> Option<char> {
|
||||
|
||||
fn create_empty_playlistitem_header(input_name: &str, url: &str) -> PlaylistItemHeader {
|
||||
PlaylistItemHeader {
|
||||
url: Rc::new(url.to_owned()),
|
||||
url: url.to_owned(),
|
||||
category_id: 0,
|
||||
input_name: Rc::new(input_name.to_string()),
|
||||
input_name: input_name.to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -87,7 +84,7 @@ macro_rules! process_header_fields {
|
||||
($header:expr, $token:expr, $(($prop:ident, $field:expr)),*; $val:expr) => {
|
||||
match $token {
|
||||
$(
|
||||
$field => $header.$prop = Rc::new($val),
|
||||
$field => $header.$prop = $val,
|
||||
)*
|
||||
_ => {}
|
||||
}
|
||||
@@ -105,7 +102,7 @@ fn process_header(input: &ConfigInput, video_suffixes: &[&str], content: &str, u
|
||||
break;
|
||||
}
|
||||
if c.unwrap() == ',' {
|
||||
plih.title = Rc::new(get_value(&mut it));
|
||||
plih.title = get_value(&mut it);
|
||||
} else {
|
||||
let token = token_till(&mut it, '=', true);
|
||||
if let Some(t) = token {
|
||||
@@ -127,11 +124,11 @@ fn process_header(input: &ConfigInput, video_suffixes: &[&str], content: &str, u
|
||||
}
|
||||
if plih.id.is_empty() {
|
||||
if let Some(chanid) = extract_id_from_url(url) {
|
||||
plih.id = Rc::new(chanid);
|
||||
plih.id = chanid;
|
||||
}
|
||||
}
|
||||
// plih.virtual_id = plih.id;
|
||||
plih.epg_channel_id = Some(Rc::clone(&plih.id));
|
||||
plih.epg_channel_id = Some(plih.id.to_string());
|
||||
}
|
||||
|
||||
if video_suffixes.iter().any(|suffix| url.ends_with(suffix)) {
|
||||
@@ -177,17 +174,16 @@ where
|
||||
continue;
|
||||
}
|
||||
if let Some(header_value) = header {
|
||||
let item = PlaylistItem { header: RefCell::new(process_header(input, &video_suffixes, &header_value, line)) };
|
||||
let mut header = item.header.borrow_mut();
|
||||
let mut item = PlaylistItem { header: process_header(input, &video_suffixes, &header_value, line) };
|
||||
let header = &mut item.header;
|
||||
if header.group.is_empty() {
|
||||
if let Some(group_value) = group {
|
||||
header.group = Rc::new(group_value);
|
||||
header.group = group_value;
|
||||
} else {
|
||||
let current_title = header.title.clone();
|
||||
header.group = Rc::new(string_utils::get_title_group(current_title.as_str()));
|
||||
header.group = string_utils::get_title_group(current_title.as_str());
|
||||
}
|
||||
}
|
||||
drop(header);
|
||||
visit(item);
|
||||
}
|
||||
header = None;
|
||||
@@ -205,7 +201,7 @@ where
|
||||
consume_m3u(cfg, input, lines, |item| {
|
||||
// keep the original sort order for groups and group the playlist items
|
||||
let key = {
|
||||
let header = item.header.borrow();
|
||||
let header = &item.header;
|
||||
format!("{}{}", &header.xtream_cluster, &header.group)
|
||||
};
|
||||
match group_map.entry(key) {
|
||||
@@ -224,9 +220,9 @@ where
|
||||
// create a group based on the first playlist item
|
||||
let channel = channels.first();
|
||||
let (cluster, group_title) = channel.map(|pli|
|
||||
(pli.header.borrow().xtream_cluster, Rc::clone(&pli.header.borrow().group))).unwrap();
|
||||
(pli.header.xtream_cluster, &pli.header.group)).unwrap();
|
||||
grp_id += 1;
|
||||
PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: Rc::clone(&group_title), channels }
|
||||
PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: group_title.to_string(), channels }
|
||||
}).collect();
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use std::sync::Arc;
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader;
|
||||
|
||||
@@ -8,14 +7,14 @@ use crate::model::xmltv::{Epg, EPG_ATTRIB_CHANNEL, EPG_ATTRIB_ID, EPG_TAG_TV, EP
|
||||
use crate::utils::compression::compressed_file_reader::CompressedFileReader;
|
||||
|
||||
impl TVGuide {
|
||||
pub fn filter(&self, channel_ids: &HashSet<Rc<String>>) -> Option<Epg> {
|
||||
pub fn filter(&self, channel_ids: &HashSet<String>) -> Option<Epg> {
|
||||
if channel_ids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match CompressedFileReader::new(&self.file) {
|
||||
Ok(mut reader) => {
|
||||
let mut children: Vec<XmlTag> = vec![];
|
||||
let mut tv_attributes: Option<Rc<HashMap<String, String>>> = None;
|
||||
let mut tv_attributes: Option<Arc<HashMap<String, String>>> = None;
|
||||
let mut filter_tags = |tag: XmlTag| {
|
||||
if match tag.name.as_str() {
|
||||
EPG_TAG_CHANNEL => {
|
||||
@@ -76,7 +75,7 @@ where
|
||||
let tag = XmlTag {
|
||||
name,
|
||||
value: None,
|
||||
attributes: if attributes.is_empty() { None } else { Some(Rc::new(attributes)) },
|
||||
attributes: if attributes.is_empty() { None } else { Some(Arc::new(attributes)) },
|
||||
children: None,
|
||||
};
|
||||
|
||||
@@ -103,7 +102,7 @@ where
|
||||
}
|
||||
} else if !stack.is_empty() {
|
||||
if let Some(old_tag) = stack.pop().map(|mut r| {
|
||||
let rc_tag = Rc::new(tag);
|
||||
let rc_tag = Arc::new(tag);
|
||||
r.children = Some(
|
||||
r.children.map_or_else(|| vec![rc_tag.clone()], |mut c| {
|
||||
c.push(rc_tag.clone());
|
||||
@@ -182,7 +181,7 @@ mod tests {
|
||||
let tv_guide = TVGuide { file: PathBuf::from(file_path) };
|
||||
|
||||
let channel_ids = vec!["channel.1", "channel.2", "channel.3"];
|
||||
let channel_ids : HashSet<Rc<String>> = channel_ids.into_iter().map(|s| Rc::new(s.to_string())).collect();
|
||||
let channel_ids : HashSet<Arc<String>> = channel_ids.into_iter().map(|s| Arc::new(s.to_string())).collect();
|
||||
|
||||
match tv_guide.filter(&channel_ids) {
|
||||
None => assert!(false, "No epg filtered"),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use std::sync::Arc;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result};
|
||||
@@ -29,13 +27,13 @@ fn map_to_xtream_streams(xtream_cluster: XtreamCluster, streams: &Value) -> Resu
|
||||
}
|
||||
}
|
||||
|
||||
fn create_xtream_series_episode_url(url: &str, username: &str, password: &str, episode: &XtreamSeriesInfoEpisode) -> Rc<String> {
|
||||
fn create_xtream_series_episode_url(url: &str, username: &str, password: &str, episode: &XtreamSeriesInfoEpisode) -> Arc<String> {
|
||||
if episode.direct_source.is_empty() {
|
||||
let ext = episode.container_extension.clone();
|
||||
let stream_base_url = format!("{url}/series/{username}/{password}/{}.{ext}", episode.id);
|
||||
Rc::new(stream_base_url)
|
||||
Arc::new(stream_base_url)
|
||||
} else {
|
||||
Rc::new(episode.direct_source.clone())
|
||||
Arc::new(episode.direct_source.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,21 +49,21 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s
|
||||
let episode_url = create_xtream_series_episode_url(url, username, password, episode);
|
||||
(episode.clone(),
|
||||
PlaylistItem {
|
||||
header: RefCell::new(PlaylistItemHeader {
|
||||
id: Rc::new(episode.id.to_string()),
|
||||
uuid: Rc::new(generate_playlist_uuid(&input.name, &episode.id, PlaylistItemType::Series, &episode_url)),
|
||||
name: Rc::new(series_name.to_string()),
|
||||
logo: Rc::new(episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string())),
|
||||
group: Rc::new(group_title.to_string()),
|
||||
title: Rc::new(episode.title.clone()),
|
||||
url: episode_url,
|
||||
header: PlaylistItemHeader {
|
||||
id: episode.id.to_string(),
|
||||
uuid: generate_playlist_uuid(&input.name, &episode.id, PlaylistItemType::Series, &episode_url),
|
||||
name: series_name.to_string(),
|
||||
logo: episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string()),
|
||||
group: group_title.to_string(),
|
||||
title: episode.title.clone(),
|
||||
url: episode_url.to_string(),
|
||||
item_type: PlaylistItemType::Series,
|
||||
xtream_cluster: XtreamCluster::Series,
|
||||
additional_properties: episode.get_additional_properties(&series_info),
|
||||
category_id: 0,
|
||||
input_name: Rc::new(input.name.to_string()),
|
||||
input_name: input.name.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
})
|
||||
}).collect();
|
||||
return if result.is_empty() { Ok(None) } else { Ok(Some(result)) };
|
||||
@@ -100,13 +98,13 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str,
|
||||
}
|
||||
|
||||
pub fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str,
|
||||
stream: &XtreamStream, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> Rc<String> {
|
||||
stream: &XtreamStream, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> String {
|
||||
if stream.direct_source.is_empty() {
|
||||
Rc::new(get_xtream_url(xtream_cluster, url, username, password, stream.get_stream_id(),
|
||||
get_xtream_url(xtream_cluster, url, username, password, stream.get_stream_id(),
|
||||
stream.container_extension.as_ref().map(std::string::ToString::to_string).as_ref(),
|
||||
live_stream_use_prefix, live_stream_without_extension))
|
||||
live_stream_use_prefix, live_stream_without_extension)
|
||||
} else {
|
||||
Rc::clone(&stream.direct_source)
|
||||
stream.direct_source.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,65 +114,63 @@ pub fn parse_xtream(input: &ConfigInput,
|
||||
streams: &Value) -> Result<Option<Vec<PlaylistGroup>>, M3uFilterError> {
|
||||
match map_to_xtream_category(categories) {
|
||||
Ok(xtream_categories) => {
|
||||
let input_name = Rc::new(input.name.to_string());
|
||||
let input_name = Arc::new(input.name.to_string());
|
||||
let url = input.url.as_str();
|
||||
let username = input.username.as_ref().map_or("", |v| v);
|
||||
let password = input.password.as_ref().map_or("", |v| v);
|
||||
|
||||
match map_to_xtream_streams(xtream_cluster, streams) {
|
||||
Ok(xtream_streams) => {
|
||||
let mut group_map: HashMap::<Rc<String>, RefCell<XtreamCategory>> =
|
||||
Ok(mut xtream_streams) => {
|
||||
let mut group_map: HashMap::<String, XtreamCategory> =
|
||||
xtream_categories.into_iter().map(|category|
|
||||
(Rc::clone(&category.category_id), RefCell::new(category))
|
||||
(category.category_id.to_string(), category)
|
||||
).collect();
|
||||
let unknown_grp = RefCell::new(XtreamCategory {
|
||||
category_id: Rc::new("0".to_string()),
|
||||
category_name: Rc::new("Unknown".to_string()),
|
||||
let mut unknown_grp = XtreamCategory {
|
||||
category_id: "0".to_string(),
|
||||
category_name: "Unknown".to_string(),
|
||||
channels: vec![],
|
||||
});
|
||||
};
|
||||
|
||||
let (live_stream_use_prefix, live_stream_without_extension) = input.options.as_ref()
|
||||
.map_or((true, false), |o| (o.xtream_live_stream_use_prefix, o.xtream_live_stream_without_extension));
|
||||
|
||||
for stream in xtream_streams {
|
||||
let group = group_map.get(&stream.category_id).unwrap_or(&unknown_grp);
|
||||
let mut grp = group.borrow_mut();
|
||||
let category_name = &grp.category_name;
|
||||
let stream_url = create_xtream_url(xtream_cluster, url, username, password, &stream, live_stream_use_prefix, live_stream_without_extension);
|
||||
for stream in &mut xtream_streams {
|
||||
let group = group_map.get_mut(&stream.category_id).unwrap_or(&mut unknown_grp);
|
||||
let category_name = &group.category_name;
|
||||
let stream_url = create_xtream_url(xtream_cluster, url, username, password, stream, live_stream_use_prefix, live_stream_without_extension);
|
||||
let item_type = PlaylistItemType::from(xtream_cluster);
|
||||
let item = PlaylistItem {
|
||||
header: RefCell::new(PlaylistItemHeader {
|
||||
id: Rc::new(stream.get_stream_id().to_string()),
|
||||
uuid: Rc::new(generate_playlist_uuid(&input_name, &stream.get_stream_id().to_string(), item_type, &stream_url)),
|
||||
name: Rc::clone(&stream.name),
|
||||
logo: Rc::clone(&stream.stream_icon),
|
||||
group: Rc::clone(category_name),
|
||||
title: Rc::clone(&stream.name),
|
||||
url: stream_url,
|
||||
header: PlaylistItemHeader {
|
||||
id: stream.get_stream_id().to_string(),
|
||||
uuid: generate_playlist_uuid(&input_name, &stream.get_stream_id().to_string(), item_type, &stream_url),
|
||||
name: stream.name.to_string(),
|
||||
logo: stream.stream_icon.to_string(),
|
||||
group: category_name.to_string(),
|
||||
title: stream.name.to_string(),
|
||||
url: stream_url.to_string(),
|
||||
epg_channel_id: stream.epg_channel_id.clone(),
|
||||
item_type,
|
||||
xtream_cluster,
|
||||
additional_properties: stream.get_additional_properties(),
|
||||
category_id: 0,
|
||||
input_name: Rc::clone(&input_name),
|
||||
input_name: input_name.to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
};
|
||||
grp.add(item);
|
||||
group.add(item);
|
||||
}
|
||||
let has_channels = !unknown_grp.borrow().channels.is_empty();
|
||||
let has_channels = !unknown_grp.channels.is_empty();
|
||||
if has_channels {
|
||||
group_map.insert(Rc::new("0".to_string()), unknown_grp);
|
||||
group_map.insert("0".to_string(), unknown_grp);
|
||||
}
|
||||
|
||||
Ok(Some(group_map.values().filter(|category| !category.borrow().channels.is_empty())
|
||||
Ok(Some(group_map.values().filter(|category| !category.channels.is_empty())
|
||||
.map(|category| {
|
||||
let cat = category.borrow();
|
||||
PlaylistGroup {
|
||||
id: cat.category_id.parse::<u32>().unwrap_or(0),
|
||||
id: category.category_id.parse::<u32>().unwrap_or(0),
|
||||
xtream_cluster,
|
||||
title: Rc::clone(&cat.category_name),
|
||||
channels: cat.channels.clone(),
|
||||
title: category.category_name.to_string(),
|
||||
channels: category.channels.clone(),
|
||||
}
|
||||
}).collect()))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::utils::file::file_utils::sanitize_filename;
|
||||
pub fn process_group_watch(cfg: &Config, target_name: &str, pl: &PlaylistGroup) {
|
||||
let mut new_tree = BTreeSet::new();
|
||||
pl.channels.iter().for_each(|chan| {
|
||||
let header = chan.header.borrow();
|
||||
let header = &chan.header;
|
||||
let title = if header.title.is_empty() { header.title.to_string() } else { header.name.to_string() };
|
||||
new_tree.insert(title);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ type AffixProcessor<'a> = Box<dyn Fn(&mut PlaylistItem) + 'a>;
|
||||
|
||||
fn create_affix_processor(affix: &InputAffix, is_prefix: bool) -> AffixProcessor {
|
||||
Box::new(move |channel: &mut PlaylistItem| {
|
||||
let header = &mut channel.header.borrow_mut();
|
||||
let header = &mut channel.header;
|
||||
let value = header.get_field(affix.field.as_str()).map_or_else(|| String::from(&affix.value), |field_value| if is_prefix {
|
||||
format!("{}{}", &affix.value, field_value.as_str())
|
||||
} else {
|
||||
|
||||
@@ -5,16 +5,13 @@ use crate::model::config::{ConfigInput, ConfigRename};
|
||||
use crate::utils::network::epg;
|
||||
use crate::utils::network::m3u;
|
||||
use crate::utils::network::xtream;
|
||||
use parking_lot::Mutex;
|
||||
use core::cmp::Ordering;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc};
|
||||
use tokio::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
use actix_rt::System;
|
||||
use log::{debug, error, info, log_enabled, trace, warn, Level};
|
||||
use std::time::Instant;
|
||||
use unidecode::unidecode;
|
||||
@@ -37,7 +34,7 @@ use crate::utils::default_utils::default_as_default;
|
||||
use crate::utils::{debug_if_enabled};
|
||||
|
||||
fn is_valid(pli: &PlaylistItem, target: &ConfigTarget) -> bool {
|
||||
let provider = ValueProvider { pli: RefCell::new(pli) };
|
||||
let provider = ValueProvider { pli };
|
||||
target.filter(&provider)
|
||||
}
|
||||
|
||||
@@ -62,8 +59,8 @@ fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt
|
||||
}
|
||||
|
||||
fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &ConfigSortGroup, match_as_ascii: bool) -> Ordering {
|
||||
let value_a = if match_as_ascii { Rc::new(unidecode(&a.title)) } else { Rc::clone(&a.title) };
|
||||
let value_b = if match_as_ascii { Rc::new(unidecode(&b.title)) } else { Rc::clone(&b.title) };
|
||||
let value_a = if match_as_ascii { unidecode(&a.title) } else { a.title.to_string() };
|
||||
let value_b = if match_as_ascii { unidecode(&b.title) } else { b.title.to_string() };
|
||||
let ordering = value_a.partial_cmp(&value_b).unwrap();
|
||||
match group_sort.order {
|
||||
Asc => ordering,
|
||||
@@ -74,8 +71,8 @@ fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &C
|
||||
fn playlistitem_comparator(a: &PlaylistItem, b: &PlaylistItem, channel_sort: &ConfigSortChannel, match_as_ascii: bool) -> Ordering {
|
||||
let raw_value_a = get_field_value(a, &channel_sort.field);
|
||||
let raw_value_b = get_field_value(b, &channel_sort.field);
|
||||
let value_a = if match_as_ascii { Rc::new(unidecode(&raw_value_a)) } else { raw_value_a };
|
||||
let value_b = if match_as_ascii { Rc::new(unidecode(&raw_value_b)) } else { raw_value_b };
|
||||
let value_a = if match_as_ascii { unidecode(&raw_value_a) } else { raw_value_a };
|
||||
let value_b = if match_as_ascii { unidecode(&raw_value_b) } else { raw_value_b };
|
||||
channel_sort.sequence.as_ref().map_or_else(|| {
|
||||
let ordering = value_a.partial_cmp(&value_b).unwrap();
|
||||
match channel_sort.order {
|
||||
@@ -84,8 +81,8 @@ fn playlistitem_comparator(a: &PlaylistItem, b: &PlaylistItem, channel_sort: &Co
|
||||
}
|
||||
}, |custom_order| {
|
||||
// Check indices in the custom order vector
|
||||
let index_a = custom_order.iter().position(|s| s == value_a.as_ref());
|
||||
let index_b = custom_order.iter().position(|s| s == value_b.as_ref());
|
||||
let index_a = custom_order.iter().position(|s| s == &value_a);
|
||||
let index_b = custom_order.iter().position(|s| s == &value_b);
|
||||
|
||||
match (index_a, index_b) {
|
||||
(Some(idx_a), Some(idx_b)) => {
|
||||
@@ -122,7 +119,7 @@ fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) {
|
||||
for channel_sort in channel_sorts {
|
||||
let regexp = channel_sort.re.as_ref().unwrap();
|
||||
for group in new_playlist.iter_mut() {
|
||||
let group_title = if match_as_ascii { Rc::new(unidecode(&group.title)) } else { Rc::clone(&group.title) };
|
||||
let group_title = if match_as_ascii { unidecode(&group.title) } else { group.title.to_string() };
|
||||
if regexp.is_match(group_title.as_str()) {
|
||||
group.channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, channel_sort, match_as_ascii));
|
||||
}
|
||||
@@ -132,17 +129,17 @@ fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_no_playlist(new_playlist: &[PlaylistGroup]) {
|
||||
fn channel_no_playlist(new_playlist: &mut [PlaylistGroup]) {
|
||||
let mut chno = 1;
|
||||
for group in new_playlist {
|
||||
for chan in &group.channels {
|
||||
chan.header.borrow_mut().chno = Rc::new(chno.to_string());
|
||||
for chan in &mut group.channels {
|
||||
chan.header.chno = chno.to_string();
|
||||
chno += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_rename(pli: &PlaylistItem, rename: Option<&Vec<ConfigRename>>) {
|
||||
fn exec_rename(pli: &mut PlaylistItem, rename: Option<&Vec<ConfigRename>>) {
|
||||
if let Some(renames) = rename {
|
||||
if !renames.is_empty() {
|
||||
let result = pli;
|
||||
@@ -153,7 +150,7 @@ fn exec_rename(pli: &PlaylistItem, rename: Option<&Vec<ConfigRename>>) {
|
||||
debug_if_enabled!("Renamed {}={} to {}", &r.field, value, cap);
|
||||
}
|
||||
let value = cap.into_owned();
|
||||
set_field_value(result, &r.field, Rc::new(value));
|
||||
set_field_value(result, &r.field, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +167,7 @@ fn rename_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt
|
||||
if matches!(r.field, ItemField::Group) {
|
||||
let cap = r.re.as_ref().unwrap().replace_all(&grp.title, &r.new_name);
|
||||
debug_if_enabled!("Renamed group {} to {} for {}", &grp.title, cap, target.name);
|
||||
grp.title = Rc::new(cap.into_owned());
|
||||
grp.title = cap.into_owned();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,19 +190,20 @@ macro_rules! apply_pattern {
|
||||
}};
|
||||
}
|
||||
|
||||
fn map_channel(channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem {
|
||||
fn map_channel(mut channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem {
|
||||
if !mapping.mapper.is_empty() {
|
||||
let header = channel.header.borrow();
|
||||
let channel_name = if mapping.match_as_ascii { Rc::new(unidecode(&header.name)) } else { header.name.clone() };
|
||||
let header = &channel.header;
|
||||
let channel_name = if mapping.match_as_ascii { unidecode(&header.name) } else { header.name.to_string() };
|
||||
if mapping.match_as_ascii && log_enabled!(Level::Trace) { trace!("Decoded {} for matching to {}", &header.name, &channel_name); };
|
||||
drop(header);
|
||||
let ref_chan = RefCell::new(&channel);
|
||||
let provider = ValueProvider { pli: ref_chan.clone() };
|
||||
// let ref_chan = &mut channel;
|
||||
let ref_chan = &mut channel;
|
||||
let mut mock_processor = MockValueProcessor {};
|
||||
for m in &mapping.mapper {
|
||||
let mut processor = MappingValueProcessor { pli: ref_chan.clone(), mapper: m };
|
||||
let provider = ValueProvider { pli: &ref_chan.clone() };
|
||||
let mut processor = MappingValueProcessor { pli: ref_chan, mapper: m };
|
||||
match &m.t_filter {
|
||||
Some(filter) => {
|
||||
|
||||
if filter.filter(&provider, &mut mock_processor) {
|
||||
apply_pattern!(&m.t_pattern, &provider, &mut processor);
|
||||
}
|
||||
@@ -235,15 +233,15 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option
|
||||
let mut grp_id: u32 = 0;
|
||||
for playlist_group in new_playlist {
|
||||
for channel in &playlist_group.channels {
|
||||
let cluster = &channel.header.borrow().xtream_cluster;
|
||||
let title = &channel.header.borrow().group;
|
||||
let cluster = &channel.header.xtream_cluster;
|
||||
let title = &channel.header.group;
|
||||
if let Some(grp) = new_groups.iter_mut().find(|x| *x.title == **title) {
|
||||
grp.channels.push(channel.clone());
|
||||
} else {
|
||||
grp_id += 1;
|
||||
new_groups.push(PlaylistGroup {
|
||||
id: grp_id,
|
||||
title: Rc::clone(title),
|
||||
title: title.to_string(),
|
||||
channels: vec![channel.clone()],
|
||||
xtream_cluster: *cluster,
|
||||
});
|
||||
@@ -256,29 +254,29 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option
|
||||
}
|
||||
}
|
||||
|
||||
fn map_playlist_counter(target: &ConfigTarget, playlist: &[PlaylistGroup]) {
|
||||
fn map_playlist_counter(target: &ConfigTarget, playlist: &mut [PlaylistGroup]) {
|
||||
if target.t_mapping.is_some() {
|
||||
let mut mock_processor = MockValueProcessor {};
|
||||
let mappings = target.t_mapping.as_ref().unwrap();
|
||||
for mapping in mappings {
|
||||
if let Some(counter_list) = &mapping.t_counter {
|
||||
for counter in counter_list {
|
||||
for plg in playlist {
|
||||
for channel in &plg.channels {
|
||||
let provider = ValueProvider { pli: RefCell::new(channel) };
|
||||
for plg in &mut *playlist {
|
||||
for channel in &mut plg.channels {
|
||||
let provider = ValueProvider { pli: channel };
|
||||
if counter.filter.filter(&provider, &mut mock_processor) {
|
||||
let cntval = counter.value.load(core::sync::atomic::Ordering::SeqCst);
|
||||
let new_value = if counter.modifier == CounterModifier::Assign {
|
||||
cntval.to_string()
|
||||
} else {
|
||||
let value = channel.header.borrow_mut().get_field(&counter.field).map_or_else(String::new, |field_value| field_value.to_string());
|
||||
let value = channel.header.get_field(&counter.field).map_or_else(String::new, |field_value| field_value.to_string());
|
||||
if counter.modifier == CounterModifier::Suffix {
|
||||
format!("{value}{}{cntval}", counter.concat)
|
||||
} else {
|
||||
format!("{cntval}{}{value}", counter.concat)
|
||||
}
|
||||
};
|
||||
channel.header.borrow_mut().set_field(&counter.field, new_value.as_str());
|
||||
channel.header.set_field(&counter.field, new_value.as_str());
|
||||
counter.value.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
@@ -401,7 +399,7 @@ async fn process_sources(client: Arc<reqwest::Client>, config: Arc<Config>, user
|
||||
for (index, _) in config.sources.iter().enumerate() {
|
||||
// We're using the file lock this way on purpose
|
||||
let source_lock_path = PathBuf::from(format!("source_{index}"));
|
||||
let Ok(update_lock) = config.file_locks.try_write_lock(&source_lock_path) else {
|
||||
let Ok(update_lock) = config.file_locks.try_write_lock(&source_lock_path).await else {
|
||||
warn!("The update operation for the source at index {index} was skipped because an update is already in progress.");
|
||||
continue;
|
||||
};
|
||||
@@ -414,11 +412,13 @@ async fn process_sources(client: Arc<reqwest::Client>, config: Arc<Config>, user
|
||||
let http_client = Arc::clone(&client);
|
||||
let handles = &mut handle_list;
|
||||
let process = move || {
|
||||
System::new().block_on(async {
|
||||
// TODO better way ?
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let (input_stats, target_stats, mut res_errors) = process_source(Arc::clone(&http_client), cfg, index, usr_trgts).await;
|
||||
shared_errors.lock().append(&mut res_errors);
|
||||
shared_errors.lock().await.append(&mut res_errors);
|
||||
let process_stats = SourceStats::new(input_stats, target_stats);
|
||||
shared_stats.lock().push(process_stats);
|
||||
shared_stats.lock().await.push(process_stats);
|
||||
});
|
||||
};
|
||||
handles.push(thread::spawn(process));
|
||||
@@ -427,9 +427,9 @@ async fn process_sources(client: Arc<reqwest::Client>, config: Arc<Config>, user
|
||||
}
|
||||
} else {
|
||||
let (input_stats, target_stats, mut res_errors) = process_source(Arc::clone(&client), cfg, index, usr_trgts).await;
|
||||
shared_errors.lock().append(&mut res_errors);
|
||||
shared_errors.lock().await.append(&mut res_errors);
|
||||
let process_stats = SourceStats::new(input_stats, target_stats);
|
||||
shared_stats.lock().push(process_stats);
|
||||
shared_stats.lock().await.push(process_stats);
|
||||
}
|
||||
drop(update_lock);
|
||||
}
|
||||
@@ -482,9 +482,9 @@ fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &FetchedP
|
||||
fn flatten_groups(playlistgroups: Vec<PlaylistGroup>) -> Vec<PlaylistGroup> {
|
||||
let mut sort_order: Vec<PlaylistGroup> = vec![];
|
||||
let mut idx: usize = 0;
|
||||
let mut group_map: HashMap<(Rc<String>, XtreamCluster), usize> = HashMap::new();
|
||||
let mut group_map: HashMap<(String, XtreamCluster), usize> = HashMap::new();
|
||||
for group in playlistgroups {
|
||||
let key = (Rc::clone(&group.title), group.xtream_cluster);
|
||||
let key = (group.title.to_string(), group.xtream_cluster);
|
||||
match group_map.entry(key) {
|
||||
std::collections::hash_map::Entry::Vacant(v) => {
|
||||
v.insert(idx);
|
||||
@@ -513,7 +513,7 @@ async fn process_playlist_for_target(client: Arc<reqwest::Client>,
|
||||
for provider_fpl in playlists.iter_mut() {
|
||||
let mut processed_fpl = execute_pipe(target, &pipe, provider_fpl, &mut duplicates);
|
||||
playlist_resolve_series(Arc::clone(&client), cfg, target, errors, &pipe, provider_fpl, &mut processed_fpl).await;
|
||||
playlist_resolve_vod(Arc::clone(&client), cfg, target, errors, &processed_fpl).await;
|
||||
playlist_resolve_vod(Arc::clone(&client), cfg, target, errors, &mut processed_fpl).await;
|
||||
// stats
|
||||
let input_stats = stats.get_mut(&processed_fpl.input.name);
|
||||
if let Some(stat) = input_stats {
|
||||
@@ -535,7 +535,7 @@ async fn process_playlist_for_target(client: Arc<reqwest::Client>,
|
||||
for mut fp in processed_fetched_playlists {
|
||||
// collect all epg_channel ids
|
||||
let epg_channel_ids: HashSet<_> = fp.playlistgroups.iter().flat_map(|g| &g.channels)
|
||||
.filter_map(|c| c.header.borrow().epg_channel_id.clone()).collect();
|
||||
.filter_map(|c| c.header.epg_channel_id.clone()).collect();
|
||||
|
||||
new_playlist.append(&mut fp.playlistgroups);
|
||||
if epg_channel_ids.is_empty() {
|
||||
@@ -554,8 +554,8 @@ async fn process_playlist_for_target(client: Arc<reqwest::Client>,
|
||||
} else {
|
||||
let mut flat_new_playlist = flatten_groups(new_playlist);
|
||||
sort_playlist(target, &mut flat_new_playlist);
|
||||
channel_no_playlist(&flat_new_playlist);
|
||||
map_playlist_counter(target, &flat_new_playlist);
|
||||
channel_no_playlist(&mut flat_new_playlist);
|
||||
map_playlist_counter(target, &mut flat_new_playlist);
|
||||
process_watch(target, cfg, &flat_new_playlist);
|
||||
persist_playlist(&mut flat_new_playlist, flatten_tvguide(&new_epg).as_ref(), target, cfg).await
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(in crate::processing) async fn playlist_resolve_download_playlist_item(clien
|
||||
};
|
||||
}
|
||||
if resolve_delay > 0 {
|
||||
actix_web::rt::time::sleep(std::time::Duration::new(u64::from(resolve_delay), 0)).await;
|
||||
tokio::time::sleep(std::time::Duration::new(u64::from(resolve_delay), 0)).await;
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -79,9 +79,9 @@ pub(in crate::processing) fn create_resolve_info_wal_files(cfg: &Config, input:
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::processing) fn should_update_info(pli: &PlaylistItem, processed_provider_ids: &HashMap<u32, u64>, field: &str) -> (bool, u32, u64) {
|
||||
let Some(provider_id) = pli.header.borrow_mut().get_provider_id() else { return (false, 0, 0) };
|
||||
let last_modified = pli.header.borrow().get_additional_property_as_u64(field);
|
||||
pub(in crate::processing) fn should_update_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap<u32, u64>, field: &str) -> (bool, u32, u64) {
|
||||
let Some(provider_id) = pli.header.get_provider_id() else { return (false, 0, 0) };
|
||||
let last_modified = pli.header.get_additional_property_as_u64(field);
|
||||
let old_timestamp = processed_provider_ids.get(&provider_id);
|
||||
(old_timestamp.is_none()
|
||||
|| last_modified.is_none()
|
||||
|
||||
@@ -52,7 +52,7 @@ fn write_series_episode_record_to_wal_file(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_update_series_info(pli: &PlaylistItem, processed_provider_ids: &HashMap<u32, u64>) -> (bool, u32, u64) {
|
||||
fn should_update_series_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap<u32, u64>) -> (bool, u32, u64) {
|
||||
should_update_info(pli, processed_provider_ids, TAG_SERIES_INFO_LAST_MODIFIED)
|
||||
}
|
||||
|
||||
@@ -69,12 +69,18 @@ async fn playlist_resolve_series_info(client: Arc<reqwest::Client>, cfg: &Config
|
||||
let mut record_writer = file_writer(&wal_record_file);
|
||||
let mut content_updated = false;
|
||||
|
||||
let series_info_iter = fpl.playlistgroups.iter()
|
||||
// TODO merge both filters to one
|
||||
let series_info_count = fpl.playlistgroups.iter()
|
||||
.filter(|&plg| plg.xtream_cluster == XtreamCluster::Series)
|
||||
.flat_map(|plg| &plg.channels)
|
||||
.filter(|&pli| pli.header.borrow().item_type == PlaylistItemType::SeriesInfo);
|
||||
.filter(|&pli| pli.header.item_type == PlaylistItemType::SeriesInfo).count();
|
||||
|
||||
let series_info_iter = fpl.playlistgroups.iter_mut()
|
||||
.filter(|plg| plg.xtream_cluster == XtreamCluster::Series)
|
||||
.flat_map(|plg| &mut plg.channels)
|
||||
.filter(|pli| pli.header.item_type == PlaylistItemType::SeriesInfo);
|
||||
|
||||
|
||||
let series_info_count = series_info_iter.clone().count();
|
||||
info!("Found {series_info_count} series info to resolve");
|
||||
let start_time = Instant::now();
|
||||
let mut processed_series_info_count = 0;
|
||||
@@ -160,21 +166,21 @@ async fn process_series_info(
|
||||
|
||||
for pli in plg
|
||||
.channels
|
||||
.iter()
|
||||
.filter(|pli| pli.header.borrow().item_type == PlaylistItemType::SeriesInfo)
|
||||
.iter_mut()
|
||||
.filter(|pli| pli.header.item_type == PlaylistItemType::SeriesInfo)
|
||||
{
|
||||
let Some(provider_id) = pli.header.borrow_mut().get_provider_id() else { continue; };
|
||||
let Some(provider_id) = pli.header.get_provider_id() else { continue; };
|
||||
let Ok(content) = info_reader.get(&provider_id) else { continue; };
|
||||
match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
Ok(series_content) => {
|
||||
let (group, series_name) = {
|
||||
let header = pli.header.borrow();
|
||||
let header = &pli.header;
|
||||
(header.group.clone(), if header.name.is_empty() {header.title.clone()} else { header.name.clone()})
|
||||
};
|
||||
match parse_xtream_series_info(&series_content, &group, &series_name, input) {
|
||||
Ok(Some(series)) => {
|
||||
for (episode, pli_episode) in &series {
|
||||
let Some(provider_id) = &pli_episode.header.borrow_mut().get_provider_id() else { continue; };
|
||||
Ok(Some(mut series)) => {
|
||||
for (episode, pli_episode) in &mut series {
|
||||
let Some(provider_id) = &pli_episode.header.get_provider_id() else { continue; };
|
||||
handle_error!(write_series_episode_record_to_wal_file(&mut wal_writer, *provider_id, episode),
|
||||
|err| errors.push(info_err!(format!("Failed to write to series episode wal file: {err}"))));
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@ fn write_vod_info_record_to_wal_file(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_update_vod_info(pli: &PlaylistItem, processed_provider_ids: &HashMap<u32, u64>) -> (bool, u32, u64) {
|
||||
fn should_update_vod_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap<u32, u64>) -> (bool, u32, u64) {
|
||||
should_update_info(pli, processed_provider_ids, TAG_VOD_INFO_ADDED)
|
||||
}
|
||||
|
||||
pub async fn playlist_resolve_vod(client: Arc<reqwest::Client>, cfg: &Config, target: &ConfigTarget, errors: &mut Vec<M3uFilterError>, fpl: &FetchedPlaylist<'_>) {
|
||||
pub async fn playlist_resolve_vod(client: Arc<reqwest::Client>, cfg: &Config, target: &ConfigTarget, errors: &mut Vec<M3uFilterError>, fpl: &mut FetchedPlaylist<'_>) {
|
||||
let (resolve_movies, resolve_delay) = get_resolve_vod_options(target, fpl);
|
||||
if !resolve_movies { return; }
|
||||
|
||||
@@ -81,11 +81,14 @@ pub async fn playlist_resolve_vod(client: Arc<reqwest::Client>, cfg: &Config, ta
|
||||
let mut record_writer = file_writer(&wal_record_file);
|
||||
let mut content_updated = false;
|
||||
|
||||
let vod_info_iter = fpl.playlistgroups.iter()
|
||||
// TODO merge both filters to one
|
||||
let vod_info_count = fpl.playlistgroups.iter()
|
||||
.flat_map(|plg| &plg.channels)
|
||||
.filter(|&pli| pli.header.borrow().xtream_cluster == XtreamCluster::Video);
|
||||
.filter(|&pli| pli.header.xtream_cluster == XtreamCluster::Video).count();
|
||||
|
||||
let vod_info_count = vod_info_iter.clone().count();
|
||||
let vod_info_iter = fpl.playlistgroups.iter_mut()
|
||||
.flat_map(|plg| plg.channels.iter_mut())
|
||||
.filter(|pli| pli.header.xtream_cluster == XtreamCluster::Video);
|
||||
|
||||
info!("Found {vod_info_count} vod info to resolve");
|
||||
let start_time = Instant::now();
|
||||
|
||||
@@ -21,8 +21,7 @@ use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tokio::fs::{create_dir_all, remove_dir, remove_file, File};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter};
|
||||
|
||||
@@ -155,7 +154,7 @@ fn trim_whitespace(pattern: &Regex, input: &str) -> String {
|
||||
pattern.replace_all(input, " ").to_string()
|
||||
}
|
||||
|
||||
fn kodi_style_rename(
|
||||
async fn kodi_style_rename(
|
||||
cfg: &Config,
|
||||
strm_item_info: &StrmItemInfo,
|
||||
style: &KodiStyle,
|
||||
@@ -189,7 +188,7 @@ fn kodi_style_rename(
|
||||
strm_item_info.input_name.as_str(),
|
||||
input_tmdb_indexes,
|
||||
strm_item_info.item_type,
|
||||
)
|
||||
).await
|
||||
}
|
||||
_ => None,
|
||||
} {
|
||||
@@ -280,7 +279,7 @@ enum InputTmdbIndexValue {
|
||||
}
|
||||
|
||||
type InputTmdbIndexMap = HashMap<String, Option<(FileReadGuard, InputTmdbIndexTree)>>;
|
||||
fn get_tmdb_value(
|
||||
async fn get_tmdb_value(
|
||||
cfg: &Config,
|
||||
provider_id: Option<u32>,
|
||||
input_name: &str,
|
||||
@@ -311,7 +310,7 @@ fn get_tmdb_value(
|
||||
.map(|storage_path| xtream_get_record_file_path(&storage_path, item_type))
|
||||
{
|
||||
{
|
||||
let file_lock = cfg.file_locks.read_lock(&tmdb_path);
|
||||
let file_lock = cfg.file_locks.read_lock(&tmdb_path).await;
|
||||
match item_type {
|
||||
PlaylistItemType::Series => {
|
||||
if let Ok(tree) =
|
||||
@@ -358,13 +357,13 @@ pub fn strm_get_file_paths(target_path: &Path) -> PathBuf {
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StrmItemInfo {
|
||||
group: Rc<String>,
|
||||
title: Rc<String>,
|
||||
group: String,
|
||||
title: String,
|
||||
item_type: PlaylistItemType,
|
||||
provider_id: Option<u32>,
|
||||
virtual_id: u32,
|
||||
input_name: Rc<String>,
|
||||
url: Rc<String>,
|
||||
input_name: String,
|
||||
url: String,
|
||||
series_name: Option<String>,
|
||||
release_date: Option<String>,
|
||||
season: Option<String>,
|
||||
@@ -378,15 +377,15 @@ impl StrmItemInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_item_info(pli: &PlaylistItem) -> StrmItemInfo {
|
||||
let mut header = pli.header.borrow_mut();
|
||||
let group = Rc::clone(&header.group);
|
||||
let title = Rc::clone(&header.title);
|
||||
fn extract_item_info(pli: &mut PlaylistItem) -> StrmItemInfo {
|
||||
let header = &mut pli.header;
|
||||
let group = header.group.to_string();
|
||||
let title = header.title.to_string();
|
||||
let item_type = header.item_type;
|
||||
let provider_id = header.get_provider_id();
|
||||
let virtual_id = header.virtual_id;
|
||||
let input_name = Rc::clone(&header.input_name);
|
||||
let url = Rc::clone(&header.url);
|
||||
let input_name = header.input_name.to_string();
|
||||
let url = header.url.to_string();
|
||||
let (series_name, release_date, added, season, episode) = match header.item_type {
|
||||
PlaylistItemType::Series => {
|
||||
let series_name = match header.get_field("name") {
|
||||
@@ -502,7 +501,7 @@ async fn cleanup_strm_output_directory(
|
||||
}
|
||||
|
||||
fn filter_strm_item(pli: &PlaylistItem) -> bool {
|
||||
let item_type = pli.header.borrow().item_type;
|
||||
let item_type = pli.header.item_type;
|
||||
item_type == PlaylistItemType::Series
|
||||
|| item_type == PlaylistItemType::Live
|
||||
|| item_type == PlaylistItemType::Video
|
||||
@@ -519,14 +518,14 @@ fn get_relative_path_str(full_path: &Path, root_path: &Path) -> String {
|
||||
}
|
||||
|
||||
struct StrmFile {
|
||||
file_name: Rc<String>,
|
||||
file_name: Arc<String>,
|
||||
dir_path: PathBuf,
|
||||
strm_info: StrmItemInfo,
|
||||
}
|
||||
|
||||
fn prepare_strm_files(
|
||||
async fn prepare_strm_files(
|
||||
cfg: &Config,
|
||||
new_playlist: &[PlaylistGroup],
|
||||
new_playlist: &mut [PlaylistGroup],
|
||||
root_path: &Path,
|
||||
underscore_whitespace: bool,
|
||||
kodi_style: bool,
|
||||
@@ -538,13 +537,13 @@ fn prepare_strm_files(
|
||||
// contains all filenames to detect collisions
|
||||
let mut all_filenames = HashSet::with_capacity(channel_count);
|
||||
// contains only collision filenames
|
||||
let mut collisions: HashSet<Rc<String>> = HashSet::new();
|
||||
let mut collisions: HashSet<Arc<String>> = HashSet::new();
|
||||
let mut input_tmdb_indexes: InputTmdbIndexMap = HashMap::with_capacity(channel_count);
|
||||
let mut result = Vec::with_capacity(channel_count);
|
||||
|
||||
// first we create the names to identify name collisions
|
||||
for pg in new_playlist {
|
||||
for pli in pg.channels.iter().filter(|&c| filter_strm_item(c)) {
|
||||
for pg in new_playlist.iter_mut() {
|
||||
for pli in pg.channels.iter_mut().filter(|c| filter_strm_item(c)) {
|
||||
let strm_item_info = extract_item_info(pli);
|
||||
let (dir_path, strm_file_name) = if kodi_style {
|
||||
kodi_style_rename(
|
||||
@@ -553,7 +552,7 @@ fn prepare_strm_files(
|
||||
&KODI_STYLE,
|
||||
&mut input_tmdb_indexes,
|
||||
underscore_whitespace,
|
||||
)
|
||||
).await
|
||||
} else {
|
||||
let dir_path = root_path.join(sanitize_for_filename(
|
||||
&strm_item_info.group,
|
||||
@@ -563,13 +562,13 @@ fn prepare_strm_files(
|
||||
sanitize_for_filename(&strm_item_info.title, underscore_whitespace);
|
||||
(dir_path, strm_file_name)
|
||||
};
|
||||
let filename = Rc::new(strm_file_name);
|
||||
let filename = Arc::new(strm_file_name);
|
||||
if all_filenames.contains(&filename) {
|
||||
collisions.insert(Rc::clone(&filename));
|
||||
collisions.insert(Arc::clone(&filename));
|
||||
};
|
||||
all_filenames.insert(Rc::clone(&filename));
|
||||
all_filenames.insert(Arc::clone(&filename));
|
||||
result.push(StrmFile {
|
||||
file_name: Rc::clone(&filename),
|
||||
file_name: Arc::clone(&filename),
|
||||
dir_path,
|
||||
strm_info: strm_item_info,
|
||||
});
|
||||
@@ -582,7 +581,7 @@ fn prepare_strm_files(
|
||||
.iter_mut()
|
||||
.filter(|s| collisions.contains(&s.file_name))
|
||||
.for_each(|s| {
|
||||
s.file_name = Rc::new(format!(
|
||||
s.file_name = Arc::new(format!(
|
||||
"{}{separator}-{separator}[{}]",
|
||||
s.file_name, s.strm_info.virtual_id
|
||||
));
|
||||
@@ -595,7 +594,7 @@ pub async fn kodi_write_strm_playlist(
|
||||
target: &ConfigTarget,
|
||||
target_output: &StrmTargetOutput,
|
||||
cfg: &Config,
|
||||
new_playlist: &[PlaylistGroup],
|
||||
new_playlist: &mut [PlaylistGroup],
|
||||
) -> Result<(), M3uFilterError> {
|
||||
if new_playlist.is_empty() {
|
||||
return Ok(());
|
||||
@@ -611,7 +610,7 @@ pub async fn kodi_write_strm_playlist(
|
||||
)));
|
||||
};
|
||||
|
||||
let credentials_and_server_info = get_credentials_and_server_info(cfg, target_output.username.as_ref());
|
||||
let credentials_and_server_info = get_credentials_and_server_info(cfg, target_output.username.as_ref()).await;
|
||||
let strm_index_path =
|
||||
strm_get_file_paths(&ensure_target_storage_path(cfg, target.name.as_str())?);
|
||||
let existing_strm = {
|
||||
@@ -640,7 +639,7 @@ pub async fn kodi_write_strm_playlist(
|
||||
&root_path,
|
||||
target_output.underscore_whitespace,
|
||||
target_output.kodi_style,
|
||||
);
|
||||
).await;
|
||||
for strm_file in strm_files {
|
||||
// file paths
|
||||
let output_path = root_path.join(&strm_file.dir_path);
|
||||
@@ -783,16 +782,16 @@ async fn has_strm_file_same_hash(file_path: &PathBuf, content_hash: UUIDType) ->
|
||||
false
|
||||
}
|
||||
|
||||
fn get_credentials_and_server_info(
|
||||
async fn get_credentials_and_server_info(
|
||||
cfg: &Config,
|
||||
username: Option<&String>,
|
||||
) -> Option<(ProxyUserCredentials, ApiProxyServerInfo)> {
|
||||
let username = username?;
|
||||
let credentials = cfg.get_user_credentials(username)?;
|
||||
let credentials = cfg.get_user_credentials(username).await?;
|
||||
if credentials.proxy != ProxyType::Reverse {
|
||||
return None;
|
||||
}
|
||||
let server_info = cfg.get_user_server_info(&credentials);
|
||||
let server_info = cfg.get_user_server_info(&credentials).await;
|
||||
Some((credentials, server_info))
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ impl M3uPlaylistIterator {
|
||||
let target_path = ensure_target_storage_path(cfg, target.name.as_str())?;
|
||||
let (m3u_path, idx_path) = m3u_get_file_paths(&target_path);
|
||||
|
||||
let file_lock = cfg.file_locks.read_lock(&m3u_path);
|
||||
let file_lock = cfg.file_locks.read_lock(&m3u_path).await;
|
||||
|
||||
let reader =
|
||||
IndexedDocumentIterator::<u32, M3uPlaylistItem>::new(&m3u_path, &idx_path)
|
||||
@@ -47,7 +47,7 @@ impl M3uPlaylistIterator {
|
||||
let filter = user_get_bouquet_filter(cfg, &user.username, None, TargetType::M3u, XtreamCluster::Live).await;
|
||||
// TODO m3u bouquet filter
|
||||
|
||||
let server_info = cfg.get_user_server_info(user);
|
||||
let server_info = cfg.get_user_server_info(user).await;
|
||||
Ok(Self {
|
||||
reader,
|
||||
base_url: server_info.get_base_url(),
|
||||
|
||||
@@ -58,7 +58,7 @@ pub async fn m3u_write_playlist( cfg: &Config, target: &ConfigTarget, target_out
|
||||
let (m3u_path, idx_path) = m3u_get_file_paths(target_path);
|
||||
let m3u_playlist = new_playlist.iter()
|
||||
.flat_map(|pg| &pg.channels)
|
||||
.filter(|&pli| pli.header.borrow().item_type != PlaylistItemType::SeriesInfo)
|
||||
.filter(|&pli| pli.header.item_type != PlaylistItemType::SeriesInfo)
|
||||
.map(PlaylistItem::to_m3u).collect::<Vec<M3uPlaylistItem>>();
|
||||
|
||||
persist_m3u_playlist_as_text(cfg, target, target_output, &m3u_playlist);
|
||||
@@ -101,13 +101,13 @@ pub async fn m3u_get_item_for_stream_id(stream_id: u32, cfg: &Config, target: &C
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_raw_m3u_playlist(config: &Arc<Config>, target: &ConfigTarget) -> Option<(FileReadGuard, impl Iterator<Item=(M3uPlaylistItem, bool)>)> {
|
||||
pub async fn iter_raw_m3u_playlist(config: &Arc<Config>, target: &ConfigTarget) -> Option<(FileReadGuard, impl Iterator<Item=(M3uPlaylistItem, bool)>)> {
|
||||
let target_path = get_target_storage_path(config, target.name.as_str())?;
|
||||
let (m3u_path, idx_path) = m3u_get_file_paths(&target_path);
|
||||
if !m3u_path.exists() || !idx_path.exists() {
|
||||
return None;
|
||||
}
|
||||
let file_lock = config.file_locks.read_lock(&m3u_path);
|
||||
let file_lock = config.file_locks.read_lock(&m3u_path).await;
|
||||
match IndexedDocumentIterator::<u32, M3uPlaylistItem>::new(&m3u_path, &idx_path)
|
||||
.map_err(|err| info_err!(format!("Could not deserialize file {m3u_path:?} - {err}"))) {
|
||||
Ok(reader) => Some((file_lock, reader)),
|
||||
|
||||
@@ -22,12 +22,12 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>,
|
||||
Err(err) => return Err(vec![err]),
|
||||
};
|
||||
|
||||
let (mut target_id_mapping, file_lock) = get_target_id_mapping(cfg, &target_path);
|
||||
let (mut target_id_mapping, file_lock) = get_target_id_mapping(cfg, &target_path).await;
|
||||
|
||||
// Virtual IDs assignment
|
||||
for group in playlist.iter_mut() {
|
||||
for channel in &group.channels {
|
||||
let mut header = channel.header.borrow_mut();
|
||||
for channel in &mut group.channels {
|
||||
let header = &mut channel.header;
|
||||
let provider_id = header.get_provider_id().unwrap_or_default();
|
||||
if provider_id == 0 {
|
||||
header.item_type = match (header.url.ends_with(HLS_EXT), header.item_type) {
|
||||
@@ -38,7 +38,7 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>,
|
||||
}
|
||||
let uuid = header.get_uuid();
|
||||
let item_type = header.item_type;
|
||||
header.virtual_id = target_id_mapping.get_and_update_virtual_id(**uuid, provider_id, item_type, 0);
|
||||
header.virtual_id = target_id_mapping.get_and_update_virtual_id(uuid, provider_id, item_type, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>,
|
||||
if errors.is_empty() { Ok(()) } else { Err(errors) }
|
||||
}
|
||||
|
||||
pub fn get_target_id_mapping(cfg: &Config, target_path: &Path) -> (TargetIdMapping, FileWriteGuard) {
|
||||
pub async fn get_target_id_mapping(cfg: &Config, target_path: &Path) -> (TargetIdMapping, FileWriteGuard) {
|
||||
let target_id_mapping_file = get_target_id_mapping_file(target_path);
|
||||
let file_lock = cfg.file_locks.write_lock(&target_id_mapping_file);
|
||||
let file_lock = cfg.file_locks.write_lock(&target_id_mapping_file).await;
|
||||
(TargetIdMapping::new(&target_id_mapping_file), file_lock)
|
||||
}
|
||||
|
||||
@@ -84,20 +84,20 @@ impl TargetIdMapping {
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn get_and_update_virtual_id(&mut self, uuid: UUIDType, provider_id: u32, item_type: PlaylistItemType, parent_virtual_id: u32) -> u32 {
|
||||
match self.by_uuid.get(&uuid) {
|
||||
pub fn get_and_update_virtual_id(&mut self, uuid: &UUIDType, provider_id: u32, item_type: PlaylistItemType, parent_virtual_id: u32) -> u32 {
|
||||
match self.by_uuid.get(uuid) {
|
||||
None => {
|
||||
self.dirty = true;
|
||||
self.virtual_id_counter += 1;
|
||||
let virtual_id = self.virtual_id_counter;
|
||||
let record = VirtualIdRecord::new(provider_id, virtual_id, item_type, parent_virtual_id, uuid);
|
||||
let record = VirtualIdRecord::new(provider_id, virtual_id, item_type, parent_virtual_id, *uuid);
|
||||
self.by_virtual_id.insert(virtual_id, record);
|
||||
self.virtual_id_counter
|
||||
}
|
||||
Some(virtual_id) => {
|
||||
if let Some(record) = self.by_virtual_id.query(virtual_id) {
|
||||
if record.provider_id == provider_id && (record.item_type != item_type || record.parent_virtual_id != parent_virtual_id) {
|
||||
let new_record = VirtualIdRecord::new(provider_id, *virtual_id, item_type, parent_virtual_id, uuid);
|
||||
let new_record = VirtualIdRecord::new(provider_id, *virtual_id, item_type, parent_virtual_id, *uuid);
|
||||
self.by_virtual_id.insert(*virtual_id, new_record);
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ impl XtreamPlaylistIterator {
|
||||
if !xtream_path.exists() || !idx_path.exists() {
|
||||
return Err(info_err!(format!("No {cluster} entries found for target {}", &target.name)));
|
||||
}
|
||||
let file_lock = config.file_locks.read_lock(&xtream_path);
|
||||
let file_lock = config.file_locks.read_lock(&xtream_path).await;
|
||||
|
||||
let reader = IndexedDocumentIterator::<u32, XtreamPlaylistItem>::new(&xtream_path, &idx_path)
|
||||
.map_err(|err| info_err!(format!("Could not deserialize file {xtream_path:?} - {err}")))?;
|
||||
|
||||
let options = XtreamMappingOptions::from_target_options(xtream_output, config);
|
||||
let server_info = config.get_user_server_info(user);
|
||||
let server_info = config.get_user_server_info(user).await;
|
||||
|
||||
let filter = user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, cluster).await;
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ pub fn xtream_get_record_file_path(storage_path: &Path, item_type: PlaylistItemT
|
||||
fn write_playlists_to_file(
|
||||
cfg: &Config,
|
||||
storage_path: &Path,
|
||||
collections: Vec<(XtreamCluster, &mut [&PlaylistItem])>,
|
||||
collections: Vec<(XtreamCluster, &[&mut PlaylistItem])>,
|
||||
) -> Result<(), M3uFilterError> {
|
||||
for (cluster, playlist) in collections {
|
||||
let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, cluster);
|
||||
@@ -137,7 +137,7 @@ fn write_playlists_to_file(
|
||||
Ok(mut writer) => {
|
||||
for item in playlist {
|
||||
let xtream = item.to_xtream();
|
||||
match writer.write_doc(item.header.borrow().virtual_id, &xtream) {
|
||||
match writer.write_doc(item.header.virtual_id, &xtream) {
|
||||
Ok(()) => {}
|
||||
Err(err) => return Err(cant_write_result!(&xtream_path, err)),
|
||||
}
|
||||
@@ -215,7 +215,7 @@ fn xtream_garbage_collect(config: &Config, target_name: &str) -> std::io::Result
|
||||
));
|
||||
{
|
||||
let _file_lock = config.file_locks.write_lock(&info_path);
|
||||
IndexedDocumentGarbageCollector::<u32>::new(info_path, idx_path)?.garbage_collect()?;
|
||||
IndexedDocumentGarbageCollector::<u32>::new(info_path.clone(), idx_path)?.garbage_collect()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -256,16 +256,14 @@ pub async fn xtream_write_playlist(
|
||||
TAG_PARENT_ID: 0
|
||||
}));
|
||||
|
||||
for pli in &plg.channels {
|
||||
let mut header = pli.header.borrow_mut();
|
||||
for pli in &mut plg.channels {
|
||||
let header = &mut pli.header;
|
||||
header.category_id = *cat_id;
|
||||
let col = match header.xtream_cluster {
|
||||
XtreamCluster::Live => &mut live_col,
|
||||
XtreamCluster::Series => &mut series_col,
|
||||
XtreamCluster::Video => &mut vod_col,
|
||||
};
|
||||
|
||||
drop(header);
|
||||
col.push(pli);
|
||||
}
|
||||
}
|
||||
@@ -288,9 +286,9 @@ pub async fn xtream_write_playlist(
|
||||
cfg,
|
||||
&path,
|
||||
vec![
|
||||
(XtreamCluster::Live, &mut live_col),
|
||||
(XtreamCluster::Video, &mut vod_col),
|
||||
(XtreamCluster::Series, &mut series_col),
|
||||
(XtreamCluster::Live, &live_col),
|
||||
(XtreamCluster::Video, &vod_col),
|
||||
(XtreamCluster::Series, &series_col),
|
||||
],
|
||||
) {
|
||||
Ok(()) => {
|
||||
@@ -430,7 +428,7 @@ pub fn xtream_write_series_info(
|
||||
|
||||
{
|
||||
let _file_lock = config.file_locks.write_lock(&info_path);
|
||||
let mut writer = IndexedDocumentWriter::new_append(info_path, idx_path)?;
|
||||
let mut writer = IndexedDocumentWriter::new_append(info_path.clone(), idx_path)?;
|
||||
writer.write_doc(series_info_id, content).map_err(|_| str_to_io_error(&format!("failed to write xtream series info for target {target_name}")))?;
|
||||
writer.store()?;
|
||||
}
|
||||
@@ -448,7 +446,7 @@ pub fn xtream_write_series_info(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn xtream_write_vod_info(
|
||||
pub async fn xtream_write_vod_info(
|
||||
config: &Config,
|
||||
target_name: &str,
|
||||
virtual_id: u32,
|
||||
@@ -457,8 +455,8 @@ pub fn xtream_write_vod_info(
|
||||
let storage_path = try_option_ok!(xtream_get_storage_path(config, target_name));
|
||||
let (info_path, idx_path) = try_option_ok!(xtream_get_info_file_paths(&storage_path, XtreamCluster::Video));
|
||||
{
|
||||
let _file_lock = config.file_locks.write_lock(&info_path);
|
||||
let mut writer = IndexedDocumentWriter::new_append(info_path, idx_path)?;
|
||||
let _file_lock = config.file_locks.write_lock(&info_path).await;
|
||||
let mut writer = IndexedDocumentWriter::new_append(info_path.clone(), idx_path)?;
|
||||
writer.write_doc(virtual_id, content).map_err(|_| str_to_io_error(&format!("failed to write xtream vod info for target {target_name}")))?;
|
||||
writer.store()?;
|
||||
}
|
||||
@@ -549,7 +547,7 @@ pub fn xtream_load_vod_info(
|
||||
None
|
||||
}
|
||||
|
||||
fn rewrite_xtream_vod_info<P>(
|
||||
async fn rewrite_xtream_vod_info<P>(
|
||||
config: &Config,
|
||||
xtream_output: &XtreamTargetOutput,
|
||||
pli: &P,
|
||||
@@ -563,7 +561,7 @@ fn rewrite_xtream_vod_info<P>(
|
||||
if let Some(Value::Object(info_data)) = doc.get_mut(TAG_INFO_DATA) {
|
||||
match user.proxy {
|
||||
ProxyType::Reverse => {
|
||||
let server_info = config.get_user_server_info(user);
|
||||
let server_info = config.get_user_server_info(user).await;
|
||||
let url = server_info.get_base_url();
|
||||
let resource_url = Some(format!("{url}/resource/movie/{}/{}/{}", user.username, user.password, pli.get_virtual_id()));
|
||||
rewrite_doc_urls(resource_url.as_ref(), info_data, INFO_REWRITE_FIELDS, INFO_RESOURCE_PREFIX);
|
||||
@@ -596,7 +594,7 @@ fn rewrite_xtream_vod_info<P>(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn rewrite_xtream_vod_info_content<P>(
|
||||
pub async fn rewrite_xtream_vod_info_content<P>(
|
||||
config: &Config,
|
||||
xtream_output: &XtreamTargetOutput,
|
||||
pli: &P,
|
||||
@@ -606,10 +604,10 @@ pub fn rewrite_xtream_vod_info_content<P>(
|
||||
P: PlaylistEntry,
|
||||
{
|
||||
let mut doc = serde_json::from_str::<Map<String, Value>>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?;
|
||||
rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc)
|
||||
rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc).await
|
||||
}
|
||||
|
||||
pub fn write_and_get_xtream_vod_info<P>(
|
||||
pub async fn write_and_get_xtream_vod_info<P>(
|
||||
config: &Config,
|
||||
target: &ConfigTarget,
|
||||
xtream_output: &XtreamTargetOutput,
|
||||
@@ -620,11 +618,11 @@ pub fn write_and_get_xtream_vod_info<P>(
|
||||
P: PlaylistEntry,
|
||||
{
|
||||
let mut doc = serde_json::from_str::<Map<String, Value>>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?;
|
||||
xtream_write_vod_info(config, target.name.as_str(), pli.get_virtual_id(), content).ok();
|
||||
rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc)
|
||||
xtream_write_vod_info(config, target.name.as_str(), pli.get_virtual_id(), content).await.ok();
|
||||
rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc).await
|
||||
}
|
||||
|
||||
fn rewrite_xtream_series_info<P>(
|
||||
async fn rewrite_xtream_series_info<P>(
|
||||
config: &Config,
|
||||
target: &ConfigTarget,
|
||||
xtream_output: &XtreamTargetOutput,
|
||||
@@ -639,7 +637,7 @@ fn rewrite_xtream_series_info<P>(
|
||||
let resource_url = if config.is_reverse_proxy_resource_rewrite_enabled() {
|
||||
match user.proxy {
|
||||
ProxyType::Reverse => {
|
||||
let server_info = config.get_user_server_info(user);
|
||||
let server_info = config.get_user_server_info(user).await;
|
||||
let url = server_info.get_base_url();
|
||||
Some(format!("{url}/resource/series/{}/{}/{}", user.username, user.password, pli.get_virtual_id()))
|
||||
}
|
||||
@@ -669,7 +667,7 @@ fn rewrite_xtream_series_info<P>(
|
||||
|
||||
let virtual_id = pli.get_virtual_id();
|
||||
{
|
||||
let (mut target_id_mapping, file_lock) = get_target_id_mapping(config, &target_path);
|
||||
let (mut target_id_mapping, file_lock) = get_target_id_mapping(config, &target_path).await;
|
||||
let options = XtreamMappingOptions::from_target_options(xtream_output, config);
|
||||
|
||||
let provider_url = pli.get_provider_url();
|
||||
@@ -679,7 +677,7 @@ fn rewrite_xtream_series_info<P>(
|
||||
{
|
||||
let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &episode_provider_id.to_string(), PlaylistItemType::Series, &provider_url);
|
||||
let episode_virtual_id = target_id_mapping.get_and_update_virtual_id(
|
||||
uuid,
|
||||
&uuid,
|
||||
episode_provider_id,
|
||||
PlaylistItemType::Series,
|
||||
virtual_id,
|
||||
@@ -710,7 +708,7 @@ fn rewrite_xtream_series_info<P>(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn rewrite_xtream_series_info_content<P>(
|
||||
pub async fn rewrite_xtream_series_info_content<P>(
|
||||
config: &Config,
|
||||
target: &ConfigTarget,
|
||||
xtream_output: &XtreamTargetOutput,
|
||||
@@ -721,10 +719,10 @@ pub fn rewrite_xtream_series_info_content<P>(
|
||||
P: PlaylistEntry,
|
||||
{
|
||||
let mut doc = serde_json::from_str::<Map<String, Value>>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?;
|
||||
rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc)
|
||||
rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc).await
|
||||
}
|
||||
|
||||
pub fn write_and_get_xtream_series_info<P>(
|
||||
pub async fn write_and_get_xtream_series_info<P>(
|
||||
config: &Config,
|
||||
target: &ConfigTarget,
|
||||
xtream_output: &XtreamTargetOutput,
|
||||
@@ -737,7 +735,7 @@ pub fn write_and_get_xtream_series_info<P>(
|
||||
let mut doc = serde_json::from_str::<Map<String, Value>>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?;
|
||||
let virtual_id = pli_series_info.get_virtual_id();
|
||||
xtream_write_series_info(config, target.name.as_str(), virtual_id, content).ok();
|
||||
rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc)
|
||||
rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc).await
|
||||
}
|
||||
|
||||
pub fn xtream_get_input_info(
|
||||
@@ -767,7 +765,7 @@ pub async fn xtream_update_input_info_file(
|
||||
{
|
||||
let _file_lock = cfg.file_locks.write_lock(&info_path);
|
||||
let mut reader = file_reader(open_readonly_file(wal_path).map_err(|err| notify_err!(format!("Could not read {cluster} info {err}")))?);
|
||||
match IndexedDocumentWriter::<u32>::new_append(info_path, idx_path) {
|
||||
match IndexedDocumentWriter::<u32>::new_append(info_path.clone(), idx_path) {
|
||||
Ok(mut writer) => {
|
||||
let mut provider_id_bytes = [0u8; 4];
|
||||
let mut length_bytes = [0u8; 4];
|
||||
@@ -925,13 +923,13 @@ pub async fn xtream_update_input_series_episodes_record_from_wal_file(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_raw_xtream_playlist(config: &Arc<Config>, target: &ConfigTarget, cluster: XtreamCluster) -> Option<(FileReadGuard, impl Iterator<Item = (XtreamPlaylistItem, bool)>)> {
|
||||
pub async fn iter_raw_xtream_playlist(config: &Arc<Config>, target: &ConfigTarget, cluster: XtreamCluster) -> Option<(FileReadGuard, impl Iterator<Item = (XtreamPlaylistItem, bool)>)> {
|
||||
if let Some(storage_path) = xtream_get_storage_path(config, target.name.as_str()) {
|
||||
let (xtream_path, idx_path) = xtream_get_file_paths(&storage_path, cluster);
|
||||
if !xtream_path.exists() || !idx_path.exists() {
|
||||
return None;
|
||||
}
|
||||
let file_lock = config.file_locks.read_lock(&xtream_path);
|
||||
let file_lock = config.file_locks.read_lock(&xtream_path).await;
|
||||
match IndexedDocumentIterator::<u32, XtreamPlaylistItem>::new(&xtream_path, &idx_path)
|
||||
.map_err(|err| info_err!(format!("Could not deserialize file {xtream_path:?} - {err}"))) {
|
||||
Ok(reader) => Some((file_lock, reader)),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::repository::storage::hash_string_as_hex;
|
||||
use crate::utils::file::file_utils::traverse_dir;
|
||||
use crate::utils::size_utils::human_readable_byte_size;
|
||||
use parking_lot::RwLock;
|
||||
use log::{debug, error, info, trace};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// `LRUResourceCache`
|
||||
///
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn read_mappings(args_mapping: Option<String>, cfg: &mut Config) -> Result<O
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_api_proxy_config(args_api_proxy_config: Option<String>, cfg: &mut Config) -> Result<Option<String>, M3uFilterError> {
|
||||
pub async fn read_api_proxy_config(args_api_proxy_config: Option<String>, cfg: &mut Config) -> Result<Option<String>, M3uFilterError> {
|
||||
let api_proxy_config_file: String = args_api_proxy_config.unwrap_or_else(|| file_utils::get_default_api_proxy_config_path(cfg.t_config_path.as_str()));
|
||||
api_proxy_config_file.clone_into(&mut cfg.t_api_proxy_file_path);
|
||||
let api_proxy_config = read_api_proxy(cfg, api_proxy_config_file.as_str(), true);
|
||||
@@ -43,7 +43,7 @@ pub fn read_api_proxy_config(args_api_proxy_config: Option<String>, cfg: &mut Co
|
||||
Ok(None)
|
||||
}
|
||||
Some(config) => {
|
||||
cfg.set_api_proxy(Some(config))?;
|
||||
cfg.set_api_proxy(Some(config)).await?;
|
||||
Ok(Some(api_proxy_config_file))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::{fmt, io};
|
||||
use std::path::{Path, PathBuf};
|
||||
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tokio::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use crate::m3u_filter_error::str_to_io_error;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -18,36 +18,36 @@ impl FileLockManager {
|
||||
}
|
||||
|
||||
// Acquires a read lock for the specified file and returns a FileReadGuard.
|
||||
pub fn read_lock(&self, path: &Path) -> FileReadGuard {
|
||||
let file_lock = self.get_or_create_lock(path);
|
||||
let guard = file_lock.read();
|
||||
pub async fn read_lock(&self, path: &Path) -> FileReadGuard {
|
||||
let file_lock = self.get_or_create_lock(path).await;
|
||||
let guard = file_lock.read().await;
|
||||
// Clone the Arc to avoid moving `file_lock` out, as it is still borrowed by `guard`
|
||||
FileReadGuard::new(Arc::clone(&file_lock), guard)
|
||||
}
|
||||
|
||||
// Acquires a write lock for the specified file and returns a FileWriteGuard.
|
||||
pub fn write_lock(&self, path: &Path) -> FileWriteGuard {
|
||||
let file_lock = self.get_or_create_lock(path);
|
||||
let guard = file_lock.write();
|
||||
pub async fn write_lock(&self, path: &Path) -> FileWriteGuard {
|
||||
let file_lock = self.get_or_create_lock(path).await;
|
||||
let guard = file_lock.write().await;
|
||||
// Clone the Arc to avoid moving `file_lock` out, as it is still borrowed by `guard`
|
||||
FileWriteGuard::new(Arc::clone(&file_lock), guard)
|
||||
}
|
||||
|
||||
// Tries to acquire a write lock for the specified file and returns a FileWriteGuard.
|
||||
pub fn try_write_lock(&self, path: &Path) -> io::Result<FileWriteGuard> {
|
||||
let file_lock = self.get_or_create_lock(path);
|
||||
pub async fn try_write_lock(&self, path: &Path) -> io::Result<FileWriteGuard> {
|
||||
let file_lock = self.get_or_create_lock(path).await;
|
||||
let guard = file_lock.try_write();
|
||||
match guard {
|
||||
// Clone the Arc to avoid moving `file_lock` out, as it is still borrowed by `guard`
|
||||
Some(lock_guard) => Ok(FileWriteGuard::new(Arc::clone(&file_lock), lock_guard)),
|
||||
None => Err(str_to_io_error("Failed to acquire write lock"))
|
||||
Ok(lock_guard) => Ok(FileWriteGuard::new(Arc::clone(&file_lock), lock_guard)),
|
||||
Err(_) => Err(str_to_io_error("Failed to acquire write lock"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper function: retrieves or creates a lock for a file.
|
||||
fn get_or_create_lock(&self, path: &Path) -> Arc<RwLock<()>> {
|
||||
let mut locks = self.locks.lock();
|
||||
async fn get_or_create_lock(&self, path: &Path) -> Arc<RwLock<()>> {
|
||||
let mut locks = self.locks.lock().await;
|
||||
|
||||
if let Some(lock) = locks.get(path) {
|
||||
return lock.clone();
|
||||
@@ -75,10 +75,11 @@ impl fmt::Debug for FileLockManager {
|
||||
}
|
||||
|
||||
// Define FileReadGuard to hold both the lock reference and the actual read guard.
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct FileReadGuard {
|
||||
lock: Arc<RwLock<()>>,
|
||||
guard: RwLockReadGuard<'static, ()>,
|
||||
guard: Arc<RwLockReadGuard<'static, ()>>,
|
||||
}
|
||||
|
||||
impl FileReadGuard {
|
||||
@@ -87,16 +88,17 @@ impl FileReadGuard {
|
||||
let static_guard: RwLockReadGuard<'static, ()> = unsafe { std::mem::transmute(guard) };
|
||||
Self {
|
||||
lock,
|
||||
guard: static_guard,
|
||||
guard: Arc::new(static_guard),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define FileWriteGuard to hold both the lock reference and the actual write guard.
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct FileWriteGuard {
|
||||
lock: Arc<RwLock<()>>,
|
||||
guard: RwLockWriteGuard<'static, ()>,
|
||||
guard: Arc<RwLockWriteGuard<'static, ()>>,
|
||||
}
|
||||
|
||||
impl FileWriteGuard {
|
||||
@@ -105,7 +107,7 @@ impl FileWriteGuard {
|
||||
let static_guard: RwLockWriteGuard<'static, ()> = unsafe { std::mem::transmute(guard) };
|
||||
Self {
|
||||
lock,
|
||||
guard: static_guard,
|
||||
guard: Arc::new(static_guard),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ pub enum MimeCategory {
|
||||
pub fn classify_content_type(headers: &[(String, String)]) -> MimeCategory {
|
||||
headers.iter()
|
||||
.find_map(|(k, v)| {
|
||||
(k == actix_web::http::header::CONTENT_TYPE.as_str()).then_some(v)
|
||||
(k == axum::http::header::CONTENT_TYPE.as_str()).then_some(v)
|
||||
})
|
||||
.map_or(MimeCategory::Unknown, |v| match v.to_lowercase().as_str() {
|
||||
v if v.starts_with("video/") || v == "application/octet-stream" => MimeCategory::Video,
|
||||
|
||||
@@ -73,27 +73,27 @@ where
|
||||
if cluster == XtreamCluster::Series {
|
||||
if let Some(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id()) {
|
||||
// Deliver existing target content
|
||||
return rewrite_xtream_series_info_content(config, target, xtream_output, pli, user, &content);
|
||||
return rewrite_xtream_series_info_content(config, target, xtream_output, pli, user, &content).await;
|
||||
}
|
||||
|
||||
// Check if the content has been resolved
|
||||
if xtream_output.resolve_series {
|
||||
if let Some(provider_id) = pli.get_provider_id() {
|
||||
if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Series) {
|
||||
return xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content);
|
||||
return xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if cluster == XtreamCluster::Video {
|
||||
if let Some(content) = xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id()) {
|
||||
// Deliver existing target content
|
||||
return rewrite_xtream_vod_info_content(config, xtream_output, pli, user, &content);
|
||||
return rewrite_xtream_vod_info_content(config, xtream_output, pli, user, &content).await;
|
||||
}
|
||||
// Check if the content has been resolved
|
||||
if xtream_output.resolve_vod {
|
||||
if let Some(provider_id) = pli.get_provider_id() {
|
||||
if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Video) {
|
||||
return xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content);
|
||||
return xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,8 +102,8 @@ where
|
||||
if let Ok(content) = get_xtream_stream_info_content(client, info_url, input).await {
|
||||
return match cluster {
|
||||
XtreamCluster::Live => Ok(content),
|
||||
XtreamCluster::Video => xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content),
|
||||
XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content),
|
||||
XtreamCluster::Video => xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content).await,
|
||||
XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content).await,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user