Replaced actix with axum

This commit is contained in:
euzu
2025-03-11 16:07:06 +01:00
parent 988f87173e
commit fe50ecbb23
60 changed files with 1774 additions and 1934 deletions
Generated
+227 -578
View File
File diff suppressed because it is too large Load Diff
+6 -8
View File
@@ -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
View File
@@ -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()
}
+15 -16
View File
@@ -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)
})))
}
+55 -45
View File
@@ -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) {
+42 -36
View File
@@ -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(&params.username, &params.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)));
}
+74 -66
View File
@@ -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))
}
+67 -46
View File
@@ -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
View File
@@ -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)
}
+43 -39
View File
@@ -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)));
// }
// }
+41 -36
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+44 -33
View File
@@ -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;
}
}
}
+22 -26
View File
@@ -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)
}
}
+7 -4
View File
@@ -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>,
-7
View File
@@ -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()
}
}
+29 -21
View File
@@ -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)
}
-7
View File
@@ -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)]
+2
View File
@@ -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}"),
}
}
}
+13 -6
View File
@@ -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}");
}
});
}
}
+2 -2
View File
@@ -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();
+3 -2
View File
@@ -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,
+5 -5
View File
@@ -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());
+28 -28
View File
@@ -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
}
}
+4 -4
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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)
}
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()));
}
+13 -17
View File
@@ -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
}
+6 -7
View File
@@ -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"),
+47 -51
View File
@@ -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()))
}
+1 -1
View File
@@ -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);
});
+1 -1
View File
@@ -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 {
+47 -47
View File
@@ -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
}
+4 -4
View File
@@ -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()
+17 -11
View File
@@ -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}"))));
}
+8 -5
View File
@@ -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();
+34 -35
View File
@@ -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))
}
+2 -2
View File
@@ -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(),
+3 -3
View File
@@ -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)),
+6 -6
View File
@@ -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)
}
+4 -4
View File
@@ -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;
}
+2 -2
View File
@@ -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;
+30 -32
View File
@@ -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 -1
View File
@@ -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`
///
+2 -2
View File
@@ -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))
}
}
+19 -17
View 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),
}
}
}
+1 -1
View File
@@ -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,
+6 -6
View File
@@ -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,
};
}