mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-17 05:12:34 +02:00
Stream sharing for live tv
This commit is contained in:
@@ -1,304 +0,0 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use actix_web::web;
|
||||
use chrono::{Duration, Local};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unidecode::unidecode;
|
||||
|
||||
use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigApi, ConfigRename, ConfigSort, ConfigTargetOptions, InputType, MessagingConfig, ProcessTargets, TargetOutput, VideoConfig, VideoDownloadConfig};
|
||||
use crate::model::config::ProcessingOrder;
|
||||
use crate::repository::storage::{hash_string_as_hex};
|
||||
|
||||
/// File-Download information.
|
||||
#[derive(Clone)]
|
||||
pub struct FileDownload {
|
||||
/// uuid of the download for identification.
|
||||
pub uuid: String,
|
||||
/// `file_dir` is the directory where the download should be placed.
|
||||
pub file_dir: PathBuf,
|
||||
/// `file_path` is the complete path including the filename.
|
||||
pub file_path: PathBuf,
|
||||
/// filename is the filename.
|
||||
pub filename: String,
|
||||
/// url is the download url.
|
||||
pub url: reqwest::Url,
|
||||
/// finished is true, if download is finished, otherweise false
|
||||
pub finished: bool,
|
||||
/// the filesize.
|
||||
pub size: u64,
|
||||
/// Optional error if something goes wrong during downloading.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Returns the directory for th file download.
|
||||
/// if option `organize_into_directories` is set, the root directory is determined.
|
||||
/// - For series, the episode pattern is used to determine the sub directory for the series.
|
||||
/// - For vod files, the title is used to determine the sub directory.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `download_cfg` the download configuration
|
||||
/// * `filestem` the prepared filestem to use as sub directory
|
||||
///
|
||||
fn get_download_directory(download_cfg: &VideoDownloadConfig, filestem: &str) -> PathBuf {
|
||||
if download_cfg.organize_into_directories {
|
||||
let mut stem = filestem;
|
||||
if let Some(re) = &download_cfg.t_re_episode_pattern {
|
||||
if let Some(captures) = re.captures(stem) {
|
||||
if let Some(episode) = captures.name("episode") {
|
||||
if !episode.as_str().is_empty() {
|
||||
stem = &stem[..episode.start()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let re_ending = download_cfg.t_re_remove_filename_ending.as_ref().unwrap();
|
||||
let dir_name = re_ending.replace(stem, "");
|
||||
let file_dir: PathBuf = [download_cfg.directory.as_ref().unwrap(), dir_name.as_ref()].iter().collect();
|
||||
file_dir
|
||||
} else {
|
||||
PathBuf::from(download_cfg.directory.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
const FILENAME_TRIM_PATTERNS: &[char] = &['.', '-', '_'];
|
||||
|
||||
impl FileDownload {
|
||||
|
||||
// TODO read header size info and restart support
|
||||
// "content-type" => ".../..."
|
||||
// "content-length" => "1975828544"
|
||||
// "accept-ranges" => "0-1975828544"
|
||||
// "content-range" => "bytes 0-1975828543/1975828544"
|
||||
|
||||
pub fn new(req_url: &str, req_filename: &str, download_cfg: &VideoDownloadConfig) -> Option<Self> {
|
||||
match reqwest::Url::parse(req_url) {
|
||||
Ok(url) => {
|
||||
let filename_re = download_cfg.t_re_filename.as_ref().unwrap();
|
||||
let tmp_filename = filename_re.replace_all(&unidecode(req_filename)
|
||||
.replace(' ', "_"), "")
|
||||
.replace("__", "_")
|
||||
.replace("_-_", "-");
|
||||
let filename_path = Path::new(&tmp_filename);
|
||||
let file_stem = filename_path.file_stem().and_then(OsStr::to_str).unwrap_or("").trim_matches(FILENAME_TRIM_PATTERNS);
|
||||
let file_ext = filename_path.extension().and_then(OsStr::to_str).unwrap_or("");
|
||||
|
||||
let mut filename = format!("{file_stem}.{file_ext}");
|
||||
let file_dir = get_download_directory(download_cfg, file_stem);
|
||||
let mut file_path: PathBuf = file_dir.clone();
|
||||
file_path.push(&filename);
|
||||
let mut x: usize = 1;
|
||||
while file_path.is_file() {
|
||||
filename = format!("{file_stem}_{x}.{file_ext}");
|
||||
file_path.clone_from(&file_dir);
|
||||
file_path.push(&filename);
|
||||
x += 1;
|
||||
}
|
||||
|
||||
file_path.to_str()?;
|
||||
|
||||
Some(Self {
|
||||
uuid: hash_string_as_hex(req_url),
|
||||
file_dir,
|
||||
file_path,
|
||||
filename,
|
||||
url,
|
||||
finished: false,
|
||||
size: 0,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct DownloadQueue {
|
||||
pub queue: Arc<Mutex<VecDeque<FileDownload>>>,
|
||||
pub active: Arc<RwLock<Option<FileDownload>>>,
|
||||
pub finished: Arc<RwLock<Vec<FileDownload>>>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub config: Arc<Config>,
|
||||
pub targets: Arc<ProcessTargets>,
|
||||
pub downloads: Arc<DownloadQueue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct XtreamUserInfo {
|
||||
pub active_cons: String,
|
||||
pub allowed_output_formats: Vec<String>,
|
||||
//["ts"],
|
||||
pub auth: u16,
|
||||
// 0 | 1
|
||||
pub created_at: i64,
|
||||
//1623429679,
|
||||
pub exp_date: i64,
|
||||
//1628755200,
|
||||
pub is_trial: String,
|
||||
// 0 | 1
|
||||
pub max_connections: String,
|
||||
pub message: String,
|
||||
pub password: String,
|
||||
pub username: String,
|
||||
pub status: String, // "Active"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct XtreamServerInfo {
|
||||
pub url: String,
|
||||
pub port: String,
|
||||
pub https_port: String,
|
||||
pub server_protocol: String,
|
||||
// http, https
|
||||
pub rtmp_port: String,
|
||||
pub timezone: String,
|
||||
pub timestamp_now: i64,
|
||||
pub time_now: String, //"2021-06-28 17:07:37"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct XtreamAuthorizationResponse {
|
||||
pub user_info: XtreamUserInfo,
|
||||
pub server_info: XtreamServerInfo,
|
||||
}
|
||||
|
||||
impl XtreamAuthorizationResponse {
|
||||
pub fn new(server_info: &ApiProxyServerInfo, user: &ProxyUserCredentials) -> Self {
|
||||
let now = Local::now();
|
||||
Self {
|
||||
user_info: XtreamUserInfo {
|
||||
active_cons: "0".to_string(),
|
||||
allowed_output_formats: Vec::from(["ts".to_string(), "m3u8".to_string(), "rtmp".to_string()]),
|
||||
auth: 1,
|
||||
created_at: (now - Duration::days(365)).timestamp(), // fake
|
||||
exp_date: (now + Duration::days(365)).timestamp(), // fake
|
||||
is_trial: "0".to_string(),
|
||||
max_connections: "1".to_string(),
|
||||
message: server_info.message.to_string(),
|
||||
password: user.password.to_string(),
|
||||
username: user.username.to_string(),
|
||||
status: "Active".to_string(),
|
||||
},
|
||||
server_info: XtreamServerInfo {
|
||||
url: server_info.host.clone(),
|
||||
port: server_info.http_port.clone(),
|
||||
https_port: server_info.https_port.clone(),
|
||||
server_protocol: server_info.protocol.clone(),
|
||||
rtmp_port: server_info.rtmp_port.clone(),
|
||||
timezone: server_info.timezone.to_string(),
|
||||
timestamp_now: now.timestamp(),
|
||||
time_now: now.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct UserApiRequest {
|
||||
#[serde(default)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub token: String,
|
||||
#[serde(default)]
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub series_id: String,
|
||||
#[serde(default)]
|
||||
pub vod_id: String,
|
||||
#[serde(default)]
|
||||
pub stream_id: String,
|
||||
#[serde(default)]
|
||||
pub category_id: String,
|
||||
#[serde(default)]
|
||||
pub limit: String,
|
||||
#[serde(default)]
|
||||
pub start: String,
|
||||
#[serde(default)]
|
||||
pub end: String,
|
||||
#[serde(default)]
|
||||
pub stream: String,
|
||||
#[serde(default)]
|
||||
pub duration: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerInputConfig {
|
||||
pub id: u16,
|
||||
pub input_type: InputType,
|
||||
pub url: String,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub persist: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerTargetConfig {
|
||||
pub id: u16,
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub options: Option<ConfigTargetOptions>,
|
||||
pub sort: Option<ConfigSort>,
|
||||
pub filter: String,
|
||||
#[serde(alias = "type")]
|
||||
pub output: Vec<TargetOutput>,
|
||||
pub rename: Option<Vec<ConfigRename>>,
|
||||
pub mapping: Option<Vec<String>>,
|
||||
pub processing_order: ProcessingOrder,
|
||||
pub watch: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerSourceConfig {
|
||||
pub inputs: Vec<ServerInputConfig>,
|
||||
pub targets: Vec<ServerTargetConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub api: ConfigApi,
|
||||
pub threads: u8,
|
||||
pub working_dir: String,
|
||||
pub backup_dir: Option<String>,
|
||||
pub schedule: Option<String>,
|
||||
pub sources: Vec<ServerSourceConfig>,
|
||||
pub messaging: Option<MessagingConfig>,
|
||||
pub video: Option<VideoConfig>,
|
||||
pub api_proxy: Option<ApiProxyConfig>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct PlaylistRequest {
|
||||
pub url: Option<String>,
|
||||
pub input_id: Option<u16>,
|
||||
}
|
||||
|
||||
impl From<web::Json<Self>> for PlaylistRequest {
|
||||
fn from(req: web::Json<Self>) -> Self {
|
||||
req.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
+44
-11
@@ -1,14 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path};
|
||||
use actix_web::http::header::{CACHE_CONTROL, HeaderValue};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use log::{debug, error, log_enabled, Level};
|
||||
use url::Url;
|
||||
use crate::api::api_model::{AppState, UserApiRequest};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::api::model::shared_stream::{NotifyStream, SharedStream};
|
||||
use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigTarget, ConfigInput};
|
||||
use crate::model::config::{Config, ConfigInput, ConfigTarget};
|
||||
use crate::utils::request_utils;
|
||||
use crate::utils::request_utils::mask_sensitive_info;
|
||||
use actix_web::http::header::{HeaderValue, CACHE_CONTROL};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use log::{debug, error, log_enabled, Level};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use url::Url;
|
||||
|
||||
pub async fn serve_file(file_path: &Path, req: &HttpRequest, mime_type: mime::Mime) -> HttpResponse {
|
||||
if file_path.exists() {
|
||||
@@ -24,7 +27,7 @@ pub async fn serve_file(file_path: &Path, req: &HttpRequest, mime_type: mime::Mi
|
||||
}
|
||||
|
||||
pub fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a UserApiRequest,
|
||||
app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> {
|
||||
app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> {
|
||||
if !username.is_empty() && !password.is_empty() {
|
||||
app_state.config.get_target_for_user(username, password)
|
||||
} else {
|
||||
@@ -49,11 +52,19 @@ pub fn get_user_server_info(cfg: &Config, user: &ProxyUserCredentials) -> ApiPro
|
||||
server_info_list.iter().find(|c| c.name.eq(server_info_name)).map_or_else(|| server_info_list.first().unwrap().clone(), std::clone::Clone::clone)
|
||||
}
|
||||
|
||||
pub async fn stream_response(stream_url: &str, req: &HttpRequest, input: Option<&ConfigInput>) -> HttpResponse {
|
||||
pub async fn stream_response(app_state: &AppState, stream_url: &str, req: &HttpRequest, input: Option<&ConfigInput>) -> HttpResponse {
|
||||
let req_headers: HashMap<&str, &[u8]> = req.headers().iter().map(|(k, v)| (k.as_str(), v.as_bytes())).collect();
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("Try to open stream {}", mask_sensitive_info(stream_url));
|
||||
}
|
||||
|
||||
if let shared_streams = app_state.shared_streams.lock().await {
|
||||
if let Some(shared_stream) = shared_streams.get(stream_url) {
|
||||
shared_stream.client_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let Ok(url) = Url::parse(stream_url) {
|
||||
let client = request_utils::get_client_request(input, &url, Some(&req_headers));
|
||||
match client.send().await {
|
||||
@@ -63,7 +74,29 @@ pub async fn stream_response(stream_url: &str, req: &HttpRequest, input: Option<
|
||||
response.headers().iter().for_each(|(k, v)| {
|
||||
response_builder.insert_header((k.as_str(), v.as_ref()));
|
||||
});
|
||||
return response_builder.body(actix_web::body::BodyStream::new(response.bytes_stream()));
|
||||
let byte_stream = Box::pin(response.bytes_stream());
|
||||
let cloned_byte_stream = Box::pin(byte_stream.clone());
|
||||
let _ = app_state.shared_streams.lock().await.insert(stream_url.to_string(),
|
||||
SharedStream {
|
||||
data_stream: byte_stream,
|
||||
client_count: AtomicU32::new(0),
|
||||
},
|
||||
);
|
||||
let (stream, notify) = NotifyStream::new(cloned_byte_stream);
|
||||
let notify_stream_url = stream_url.to_string();
|
||||
actix_rt::spawn(async move {
|
||||
let _ = notify.await;
|
||||
debug!("connection closed {notify_stream_url}");
|
||||
if let mut shared_streams = app_state.shared_streams.lock().await {
|
||||
if let Some(shared_stream) = shared_streams.get(stream_url) {
|
||||
let cur_count = shared_stream.client_count.fetch_sub(1u32, std::sync::atomic::Ordering::SeqCst);
|
||||
if cur_count == 1 {
|
||||
shared_streams.remove(stream_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return response_builder.body(actix_web::body::BodyStream::new(stream));
|
||||
}
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("Failed to open stream got status {} for {}", response.status(), mask_sensitive_info(stream_url));
|
||||
|
||||
+28
-26
@@ -1,18 +1,20 @@
|
||||
use crate::model::config::VideoDownloadConfig;
|
||||
use crate::utils::request_utils;
|
||||
use actix_web::{web, HttpResponse};
|
||||
use futures::stream::TryStreamExt;
|
||||
use log::info;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs::File;
|
||||
use std::{fs, io};
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde_json::{json, Value};
|
||||
use crate::api::api_model::{AppState, DownloadQueue, FileDownload, FileDownloadRequest};
|
||||
use crate::model::config::{VideoDownloadConfig};
|
||||
use futures::stream::TryStreamExt;
|
||||
use log::{info};
|
||||
use crate::utils::{request_utils};
|
||||
use std::sync::{Arc};
|
||||
use async_std::sync::RwLock;
|
||||
use std::{fs, io};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::download::{DownloadQueue, FileDownload, FileDownloadRequest};
|
||||
|
||||
async fn download_file(active: Arc<RwLock<Option<FileDownload>>>, client: &reqwest::Client) -> Result<(), String> {
|
||||
let file_download = { active.read().unwrap().as_ref().unwrap().clone() };
|
||||
let file_download = { active.read().await.as_ref().unwrap().clone() };
|
||||
match client.get(file_download.url.clone()).send().await {
|
||||
Ok(response) => {
|
||||
match fs::create_dir_all(&file_download.file_dir) {
|
||||
@@ -30,14 +32,14 @@ async fn download_file(active: Arc<RwLock<Option<FileDownload>>>, client: &reqwe
|
||||
match file.write_all(&chunk) {
|
||||
Ok(()) => {
|
||||
downloaded += chunk.len() as u64;
|
||||
active.write().unwrap().as_mut().unwrap().size = downloaded;
|
||||
active.write().await.as_mut().unwrap().size = downloaded;
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {file_path_str} {err}"))
|
||||
}
|
||||
} else {
|
||||
let megabytes = request_utils::bytes_to_megabytes(downloaded);
|
||||
info!("Downloaded {}, filesize: {}MB", file_path_str, megabytes);
|
||||
active.write().unwrap().as_mut().unwrap().size = downloaded;
|
||||
active.write().await.as_mut().unwrap().size = downloaded;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -58,33 +60,33 @@ async fn download_file(active: Arc<RwLock<Option<FileDownload>>>, client: &reqwe
|
||||
}
|
||||
}
|
||||
|
||||
fn run_download_queue(download_cfg: &VideoDownloadConfig, download_queue: &Arc<DownloadQueue>) -> Result<(), String> {
|
||||
let next_download = download_queue.as_ref().queue.lock().unwrap().pop_front();
|
||||
async fn run_download_queue(download_cfg: &VideoDownloadConfig, download_queue: &Arc<DownloadQueue>) -> Result<(), String> {
|
||||
let next_download = download_queue.as_ref().queue.lock().await.pop_front();
|
||||
if next_download.is_some() {
|
||||
{ *download_queue.as_ref().active.write().unwrap() = next_download; }
|
||||
{ *download_queue.as_ref().active.write().await = next_download; }
|
||||
let headers = request_utils::get_request_headers(Some(&download_cfg.headers), None);
|
||||
let dq = Arc::clone(download_queue);
|
||||
match reqwest::Client::builder().default_headers(headers).build() {
|
||||
Ok(client) => {
|
||||
actix_rt::spawn(async move {
|
||||
loop {
|
||||
if dq.active.read().unwrap().deref().is_some() {
|
||||
if dq.active.read().await.deref().is_some() {
|
||||
match download_file(Arc::clone(&dq.active), &client).await {
|
||||
Ok(()) => {
|
||||
if let Some(fd) = &mut *dq.active.write().unwrap() {
|
||||
if let Some(fd) = &mut *dq.active.write().await {
|
||||
fd.finished = true;
|
||||
dq.finished.write().unwrap().push(fd.clone());
|
||||
dq.finished.write().await.push(fd.clone());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(fd) = &mut *dq.active.write().unwrap() {
|
||||
if let Some(fd) = &mut *dq.active.write().await {
|
||||
fd.finished = true;
|
||||
fd.error = Some(err);
|
||||
dq.finished.write().unwrap().push(fd.clone());
|
||||
dq.finished.write().await.push(fd.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
*dq.active.write().unwrap() = dq.queue.lock().unwrap().pop_front();
|
||||
*dq.active.write().await = dq.queue.lock().await.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -117,9 +119,9 @@ pub async fn queue_download_file(
|
||||
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().unwrap().push_back(file_download);
|
||||
if app_state.downloads.active.read().unwrap().is_none() {
|
||||
match run_download_queue(download_cfg, &app_state.downloads) {
|
||||
app_state.downloads.queue.lock().await.push_back(file_download);
|
||||
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})),
|
||||
}
|
||||
@@ -136,10 +138,10 @@ pub async fn queue_download_file(
|
||||
pub async fn download_file_info(
|
||||
app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
let finished_list: &[Value] = &app_state.downloads.finished.write().unwrap().drain(..)
|
||||
let finished_list: &[Value] = &app_state.downloads.finished.write().await.drain(..)
|
||||
.map(|fd| download_info!(fd)).collect::<Vec<Value>>();
|
||||
|
||||
(*app_state.downloads.active.read().unwrap()).as_ref().map_or_else(|| HttpResponse::Ok().json(json!({
|
||||
(*app_state.downloads.active.read().await).as_ref().map_or_else(|| HttpResponse::Ok().json(json!({
|
||||
"completed": true, "downloads": finished_list
|
||||
})), |file_download| HttpResponse::Ok().json(json!({
|
||||
"completed": false, "downloads": finished_list, "active": download_info!(file_download)
|
||||
|
||||
+8
-7
@@ -4,20 +4,21 @@ use futures::{stream};
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, stream_response};
|
||||
use crate::api::api_model::{AppState, UserApiRequest};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::model::api_proxy::ProxyType;
|
||||
use crate::model::config::TargetType;
|
||||
use crate::repository::m3u_repository::{m3u_get_file_paths, m3u_get_item_for_stream_id, m3u_load_rewrite_playlist};
|
||||
use crate::repository::storage::get_target_storage_path;
|
||||
use crate::utils::request_utils::mask_sensitive_info;
|
||||
|
||||
fn m3u_api(
|
||||
async fn m3u_api(
|
||||
api_req: &UserApiRequest,
|
||||
app_state: &AppState,
|
||||
) -> HttpResponse {
|
||||
match get_user_target(api_req, app_state) {
|
||||
Some((user, target)) => {
|
||||
match m3u_load_rewrite_playlist(&app_state.config, target, &user) {
|
||||
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(format!("{line}\n")))));
|
||||
@@ -38,13 +39,13 @@ fn m3u_api(
|
||||
async fn m3u_api_get( api_req: web::Query<UserApiRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
m3u_api(&api_req.into_inner(), &app_state)
|
||||
m3u_api(&api_req.into_inner(), &app_state).await
|
||||
}
|
||||
async fn m3u_api_post(
|
||||
api_req: web::Form<UserApiRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
m3u_api(&api_req.into_inner(), &app_state)
|
||||
m3u_api(&api_req.into_inner(), &app_state).await
|
||||
}
|
||||
|
||||
async fn m3u_api_stream(
|
||||
@@ -60,14 +61,14 @@ async fn m3u_api_stream(
|
||||
match get_target_storage_path(&app_state.config, target.name.as_str()) {
|
||||
Some(target_path) => {
|
||||
let (m3u_path, idx_path) = m3u_get_file_paths(&target_path);
|
||||
match m3u_get_item_for_stream_id(&app_state.config, m3u_stream_id, &m3u_path, &idx_path) {
|
||||
match m3u_get_item_for_stream_id(&app_state.config, m3u_stream_id, &m3u_path, &idx_path).await {
|
||||
Ok(m3u_item) => {
|
||||
if user.proxy == ProxyType::Redirect {
|
||||
let stream_url = m3u_item.url;
|
||||
debug!("Redirecting stream request to {}", mask_sensitive_info(&stream_url));
|
||||
return HttpResponse::Found().insert_header(("Location", stream_url.to_string())).finish();
|
||||
}
|
||||
return stream_response(m3u_item.url.as_str(), &req, None).await;
|
||||
return stream_response(&app_state, m3u_item.url.as_str(), &req, None).await;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to get m3u url: {}", mask_sensitive_info(err.to_string().as_str()));
|
||||
|
||||
+10
-8
@@ -1,15 +1,16 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use std::sync::{Arc};
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpResponse, HttpServer, web};
|
||||
use actix_web::middleware::{Logger};
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::{web, App, HttpResponse, HttpServer};
|
||||
use async_std::sync::{RwLock, Mutex};
|
||||
use log::info;
|
||||
|
||||
use crate::api::api_model::{AppState, DownloadQueue};
|
||||
use crate::api::m3u_api::m3u_api_register;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::download::DownloadQueue;
|
||||
use crate::api::scheduler::start_scheduler;
|
||||
use crate::api::v1_api::v1_api_register;
|
||||
use crate::api::web_index::index_register;
|
||||
@@ -32,10 +33,10 @@ fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result<PathBuf, std
|
||||
|
||||
async fn healthcheck() -> HttpResponse {
|
||||
let ts = chrono::offset::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
HttpResponse::Ok().json( Healthcheck {
|
||||
HttpResponse::Ok().json(Healthcheck {
|
||||
status: "ok".to_string(),
|
||||
version: VERSION.to_string(),
|
||||
time: ts
|
||||
time: ts,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ pub async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>) -> fut
|
||||
active: Arc::from(RwLock::new(None)),
|
||||
finished: Arc::from(RwLock::new(Vec::new())),
|
||||
}),
|
||||
shared_streams: Arc::new(Mutex::new(HashMap::new())),
|
||||
});
|
||||
|
||||
// Scheduler
|
||||
|
||||
+3
-2
@@ -1,5 +1,4 @@
|
||||
pub mod api_utils;
|
||||
pub mod api_model;
|
||||
pub mod main_api;
|
||||
mod download_api;
|
||||
mod v1_api;
|
||||
@@ -7,4 +6,6 @@ mod xtream_api;
|
||||
mod m3u_api;
|
||||
mod xmltv_api;
|
||||
mod scheduler;
|
||||
mod web_index;
|
||||
mod web_index;
|
||||
|
||||
pub(crate) mod model;
|
||||
@@ -0,0 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use async_std::sync::{Mutex};
|
||||
use crate::api::model::download::DownloadQueue;
|
||||
use crate::api::model::shared_stream::SharedStream;
|
||||
use crate::model::config::{Config, ProcessTargets};
|
||||
|
||||
pub struct AppState {
|
||||
pub config: Arc<Config>,
|
||||
pub targets: Arc<ProcessTargets>,
|
||||
pub downloads: Arc<DownloadQueue>,
|
||||
pub shared_streams: Arc<Mutex<HashMap<String, SharedStream>>>,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::api_proxy::ApiProxyConfig;
|
||||
use crate::model::config::ProcessingOrder;
|
||||
use crate::model::config::{ConfigApi, ConfigRename, ConfigSort, ConfigTargetOptions, InputType, MessagingConfig, TargetOutput, VideoConfig};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerInputConfig {
|
||||
pub id: u16,
|
||||
pub input_type: InputType,
|
||||
pub url: String,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub persist: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerTargetConfig {
|
||||
pub id: u16,
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub options: Option<ConfigTargetOptions>,
|
||||
pub sort: Option<ConfigSort>,
|
||||
pub filter: String,
|
||||
#[serde(alias = "type")]
|
||||
pub output: Vec<TargetOutput>,
|
||||
pub rename: Option<Vec<ConfigRename>>,
|
||||
pub mapping: Option<Vec<String>>,
|
||||
pub processing_order: ProcessingOrder,
|
||||
pub watch: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerSourceConfig {
|
||||
pub inputs: Vec<ServerInputConfig>,
|
||||
pub targets: Vec<ServerTargetConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub api: ConfigApi,
|
||||
pub threads: u8,
|
||||
pub working_dir: String,
|
||||
pub backup_dir: Option<String>,
|
||||
pub schedule: Option<String>,
|
||||
pub sources: Vec<ServerSourceConfig>,
|
||||
pub messaging: Option<MessagingConfig>,
|
||||
pub video: Option<VideoConfig>,
|
||||
pub api_proxy: Option<ApiProxyConfig>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc};
|
||||
use async_std::sync::{RwLock, Mutex};
|
||||
use actix_web::web;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unidecode::unidecode;
|
||||
|
||||
use crate::model::config::VideoDownloadConfig;
|
||||
use crate::repository::storage::hash_string_as_hex;
|
||||
|
||||
/// File-Download information.
|
||||
#[derive(Clone)]
|
||||
pub struct FileDownload {
|
||||
/// uuid of the download for identification.
|
||||
pub uuid: String,
|
||||
/// `file_dir` is the directory where the download should be placed.
|
||||
pub file_dir: PathBuf,
|
||||
/// `file_path` is the complete path including the filename.
|
||||
pub file_path: PathBuf,
|
||||
/// filename is the filename.
|
||||
pub filename: String,
|
||||
/// url is the download url.
|
||||
pub url: reqwest::Url,
|
||||
/// finished is true, if download is finished, otherweise false
|
||||
pub finished: bool,
|
||||
/// the filesize.
|
||||
pub size: u64,
|
||||
/// Optional error if something goes wrong during downloading.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Returns the directory for th file download.
|
||||
/// if option `organize_into_directories` is set, the root directory is determined.
|
||||
/// - For series, the episode pattern is used to determine the sub directory for the series.
|
||||
/// - For vod files, the title is used to determine the sub directory.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `download_cfg` the download configuration
|
||||
/// * `filestem` the prepared filestem to use as sub directory
|
||||
///
|
||||
fn get_download_directory(download_cfg: &VideoDownloadConfig, filestem: &str) -> PathBuf {
|
||||
if download_cfg.organize_into_directories {
|
||||
let mut stem = filestem;
|
||||
if let Some(re) = &download_cfg.t_re_episode_pattern {
|
||||
if let Some(captures) = re.captures(stem) {
|
||||
if let Some(episode) = captures.name("episode") {
|
||||
if !episode.as_str().is_empty() {
|
||||
stem = &stem[..episode.start()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let re_ending = download_cfg.t_re_remove_filename_ending.as_ref().unwrap();
|
||||
let dir_name = re_ending.replace(stem, "");
|
||||
let file_dir: PathBuf = [download_cfg.directory.as_ref().unwrap(), dir_name.as_ref()].iter().collect();
|
||||
file_dir
|
||||
} else {
|
||||
PathBuf::from(download_cfg.directory.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
const FILENAME_TRIM_PATTERNS: &[char] = &['.', '-', '_'];
|
||||
|
||||
impl FileDownload {
|
||||
|
||||
// TODO read header size info and restart support
|
||||
// "content-type" => ".../..."
|
||||
// "content-length" => "1975828544"
|
||||
// "accept-ranges" => "0-1975828544"
|
||||
// "content-range" => "bytes 0-1975828543/1975828544"
|
||||
|
||||
pub fn new(req_url: &str, req_filename: &str, download_cfg: &VideoDownloadConfig) -> Option<Self> {
|
||||
match reqwest::Url::parse(req_url) {
|
||||
Ok(url) => {
|
||||
let filename_re = download_cfg.t_re_filename.as_ref().unwrap();
|
||||
let tmp_filename = filename_re.replace_all(&unidecode(req_filename)
|
||||
.replace(' ', "_"), "")
|
||||
.replace("__", "_")
|
||||
.replace("_-_", "-");
|
||||
let filename_path = Path::new(&tmp_filename);
|
||||
let file_stem = filename_path.file_stem().and_then(OsStr::to_str).unwrap_or("").trim_matches(FILENAME_TRIM_PATTERNS);
|
||||
let file_ext = filename_path.extension().and_then(OsStr::to_str).unwrap_or("");
|
||||
|
||||
let mut filename = format!("{file_stem}.{file_ext}");
|
||||
let file_dir = get_download_directory(download_cfg, file_stem);
|
||||
let mut file_path: PathBuf = file_dir.clone();
|
||||
file_path.push(&filename);
|
||||
let mut x: usize = 1;
|
||||
while file_path.is_file() {
|
||||
filename = format!("{file_stem}_{x}.{file_ext}");
|
||||
file_path.clone_from(&file_dir);
|
||||
file_path.push(&filename);
|
||||
x += 1;
|
||||
}
|
||||
|
||||
file_path.to_str()?;
|
||||
|
||||
Some(Self {
|
||||
uuid: hash_string_as_hex(req_url),
|
||||
file_dir,
|
||||
file_path,
|
||||
filename,
|
||||
url,
|
||||
finished: false,
|
||||
size: 0,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DownloadQueue {
|
||||
pub queue: Arc<Mutex<VecDeque<FileDownload>>>,
|
||||
pub active: Arc<RwLock<Option<FileDownload>>>,
|
||||
pub finished: Arc<RwLock<Vec<FileDownload>>>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod request;
|
||||
pub mod config;
|
||||
pub mod download;
|
||||
pub mod xtream;
|
||||
pub mod app_state;
|
||||
pub mod shared_stream;
|
||||
@@ -0,0 +1,44 @@
|
||||
use actix_web::web;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct PlaylistRequest {
|
||||
pub url: Option<String>,
|
||||
pub input_id: Option<u16>,
|
||||
}
|
||||
|
||||
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)]
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub token: String,
|
||||
#[serde(default)]
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub series_id: String,
|
||||
#[serde(default)]
|
||||
pub vod_id: String,
|
||||
#[serde(default)]
|
||||
pub stream_id: String,
|
||||
#[serde(default)]
|
||||
pub category_id: String,
|
||||
#[serde(default)]
|
||||
pub limit: String,
|
||||
#[serde(default)]
|
||||
pub start: String,
|
||||
#[serde(default)]
|
||||
pub end: String,
|
||||
#[serde(default)]
|
||||
pub stream: String,
|
||||
#[serde(default)]
|
||||
pub duration: String,
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use futures::stream::{Stream};
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use bytes::Bytes;
|
||||
use futures::channel::oneshot::{channel, Receiver, Sender};
|
||||
use futures::StreamExt;
|
||||
|
||||
pub struct NotifyStream<S> {
|
||||
stream: S,
|
||||
tx: Option<Sender<()>>,
|
||||
}
|
||||
|
||||
impl<S, E> NotifyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>>
|
||||
{
|
||||
pub fn new(stream: S) -> (Self, Receiver<()>) {
|
||||
let (send, recv) = channel();
|
||||
(NotifyStream {
|
||||
stream,
|
||||
tx: Some(send),
|
||||
}, recv)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> Stream for NotifyStream<S> where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
type Item = S::Item;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<S::Item>> {
|
||||
let result = self.stream.poll_next_unpin(cx);
|
||||
let mut connection_closed = false;
|
||||
match &result {
|
||||
Poll::Ready(val) => {
|
||||
if val.is_none() {
|
||||
connection_closed = true;
|
||||
}
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
if connection_closed {
|
||||
if let Some(send) = self.tx.take() {
|
||||
// Ignore errors as they just mean the receiver was dropped.
|
||||
let _ = send.send(());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SharedStream {
|
||||
pub data_stream: Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>>>>,
|
||||
pub client_count: AtomicU32,
|
||||
}
|
||||
|
||||
//
|
||||
// async fn stream_handler(
|
||||
// state: web::Data<SharedState>,
|
||||
// _req: HttpRequest,
|
||||
// ) -> impl Responder {
|
||||
// let mut shared_state = state.lock().unwrap();
|
||||
//
|
||||
// // Check if a shared resource already exists
|
||||
// if let Some(resource) = shared_state.as_mut() {
|
||||
// // Increment client count
|
||||
// resource.client_count += 1;
|
||||
//
|
||||
// // Create a stream for this client
|
||||
// let client_stream = resource.data_stream.clone();
|
||||
// return HttpResponse::Ok().streaming(client_stream);
|
||||
// }
|
||||
//
|
||||
// // If no shared resource, create one
|
||||
// let data_stream = crate::api::model::shared_stream::create_data_stream(); // Replace with your actual stream logic
|
||||
// let boxed_stream = Box::pin(data_stream);
|
||||
//
|
||||
// *shared_state = Some(SharedStream {
|
||||
// data_stream: boxed_stream.clone(),
|
||||
// client_count: 1,
|
||||
// });
|
||||
//
|
||||
// // Respond with the stream for this client
|
||||
// HttpResponse::Ok().streaming(boxed_stream)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// async fn cleanup_on_disconnect(state: web::Data<SharedState>) {
|
||||
// let mut shared_state = state.lock().unwrap();
|
||||
// if let Some(resource) = shared_state.as_mut() {
|
||||
// resource.client_count -= 1;
|
||||
// if resource.client_count == 0 {
|
||||
// // Drop the shared resource if no clients are left
|
||||
// *shared_state = None;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials};
|
||||
use serde::Serialize;
|
||||
use chrono::{Duration, Local};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct XtreamUserInfo {
|
||||
pub active_cons: String,
|
||||
pub allowed_output_formats: Vec<String>,
|
||||
//["ts"],
|
||||
pub auth: u16,
|
||||
// 0 | 1
|
||||
pub created_at: i64,
|
||||
//1623429679,
|
||||
pub exp_date: i64,
|
||||
//1628755200,
|
||||
pub is_trial: String,
|
||||
// 0 | 1
|
||||
pub max_connections: String,
|
||||
pub message: String,
|
||||
pub password: String,
|
||||
pub username: String,
|
||||
pub status: String, // "Active"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct XtreamServerInfo {
|
||||
pub url: String,
|
||||
pub port: String,
|
||||
pub https_port: String,
|
||||
pub server_protocol: String,
|
||||
// http, https
|
||||
pub rtmp_port: String,
|
||||
pub timezone: String,
|
||||
pub timestamp_now: i64,
|
||||
pub time_now: String, //"2021-06-28 17:07:37"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct XtreamAuthorizationResponse {
|
||||
pub user_info: XtreamUserInfo,
|
||||
pub server_info: XtreamServerInfo,
|
||||
}
|
||||
|
||||
impl XtreamAuthorizationResponse {
|
||||
pub fn new(server_info: &ApiProxyServerInfo, user: &ProxyUserCredentials) -> Self {
|
||||
let now = Local::now();
|
||||
Self {
|
||||
user_info: XtreamUserInfo {
|
||||
active_cons: "0".to_string(),
|
||||
allowed_output_formats: Vec::from(["ts".to_string(), "m3u8".to_string(), "rtmp".to_string()]),
|
||||
auth: 1,
|
||||
created_at: (now - Duration::days(365)).timestamp(), // fake
|
||||
exp_date: (now + Duration::days(365)).timestamp(), // fake
|
||||
is_trial: "0".to_string(),
|
||||
max_connections: "1".to_string(),
|
||||
message: server_info.message.to_string(),
|
||||
password: user.password.to_string(),
|
||||
username: user.username.to_string(),
|
||||
status: "Active".to_string(),
|
||||
},
|
||||
server_info: XtreamServerInfo {
|
||||
url: server_info.host.clone(),
|
||||
port: server_info.http_port.clone(),
|
||||
https_port: server_info.https_port.clone(),
|
||||
server_protocol: server_info.protocol.clone(),
|
||||
rtmp_port: server_info.rtmp_port.clone(),
|
||||
timezone: server_info.timezone.to_string(),
|
||||
timestamp_now: now.timestamp(),
|
||||
time_now: now.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use actix_web::web::Data;
|
||||
use chrono::{DateTime, FixedOffset, Local};
|
||||
use cron::Schedule;
|
||||
use log::error;
|
||||
use crate::api::api_model::AppState;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::exit;
|
||||
use crate::processing::playlist_processor::exec_processing;
|
||||
|
||||
|
||||
+7
-5
@@ -1,20 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::{HttpResponse, web};
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web::{web, HttpResponse};
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use log::error;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::api::api_model::{AppState, PlaylistRequest, ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig};
|
||||
use crate::api::download_api;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::config::{ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig};
|
||||
use crate::api::model::request::PlaylistRequest;
|
||||
use crate::auth::authenticator::validator;
|
||||
use crate::m3u_filter_error::M3uFilterError;
|
||||
use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials, TargetUser};
|
||||
use crate::model::config::{Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType, validate_targets};
|
||||
use crate::model::config::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType};
|
||||
use crate::processing::playlist_processor;
|
||||
use crate::utils::{config_reader, download};
|
||||
use crate::utils::request_utils::mask_sensitive_info;
|
||||
use crate::utils::{config_reader, download};
|
||||
|
||||
fn intern_save_config_api_proxy(backup_dir: &str, api_proxy: &ApiProxyConfig, file_path: &str) -> Option<M3uFilterError> {
|
||||
match config_reader::save_api_proxy(file_path, backup_dir, api_proxy) {
|
||||
@@ -155,7 +157,7 @@ async fn playlist(
|
||||
let url = req.url.as_deref().unwrap_or("");
|
||||
let input = create_config_input_for_url(url);
|
||||
get_playlist(Some(&input), &app_state.config).await
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async fn config(
|
||||
|
||||
@@ -4,8 +4,7 @@ use std::path::{Path, PathBuf};
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
|
||||
use crate::api::api_model::AppState;
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::auth::authenticator::{create_jwt, verify_token};
|
||||
use crate::auth::password::verify_password;
|
||||
use crate::auth::user::UserCredential;
|
||||
|
||||
@@ -10,8 +10,9 @@ use quick_xml::events::{BytesStart, Event};
|
||||
use std::io::{BufReader};
|
||||
use chrono::{Duration, NaiveDateTime, TimeDelta};
|
||||
|
||||
use crate::api::api_model::{AppState, UserApiRequest};
|
||||
use crate::api::api_utils::{get_user_target, serve_file};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::model::api_proxy::{ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigTarget};
|
||||
use crate::model::config::TargetType;
|
||||
|
||||
+35
-33
@@ -6,26 +6,28 @@ use std::io::{Error, ErrorKind};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use futures::Stream;
|
||||
use log::{debug, error, log_enabled, Level};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::api::api_model::{AppState, UserApiRequest, XtreamAuthorizationResponse};
|
||||
use crate::api::api_utils::{get_user_server_info, get_user_target, get_user_target_by_credentials, serve_file, stream_response};
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::api::model::request::UserApiRequest;
|
||||
use crate::api::model::xtream::XtreamAuthorizationResponse;
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::model::api_proxy::{ProxyType, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigInput, ConfigTarget};
|
||||
use crate::model::config::TargetType;
|
||||
use crate::model::config::{Config, ConfigInput, ConfigTarget};
|
||||
use crate::model::playlist::{PlaylistItemType, XtreamCluster, XtreamPlaylistItem};
|
||||
use crate::model::xtream::XtreamMappingOptions;
|
||||
use crate::repository::storage::{get_target_storage_path, hash_string};
|
||||
use crate::repository::target_id_mapping::TargetIdMapping;
|
||||
use crate::repository::xtream_repository;
|
||||
use crate::utils::{json_utils, request_utils};
|
||||
use crate::utils::request_utils::mask_sensitive_info;
|
||||
use crate::utils::{json_utils, request_utils};
|
||||
|
||||
const ACTION_GET_SERIES_INFO: &str = "get_series_info";
|
||||
const ACTION_GET_VOD_INFO: &str = "get_vod_info";
|
||||
@@ -35,7 +37,7 @@ const ACTION_GET_SHORT_EPG: &str = "get_short_epg";
|
||||
const ACTION_GET_CATCHUP_TABLE: &str = "get_simple_data_table";
|
||||
const ACTION_GET_LIVE_CATEGORIES: &str = "get_live_categories";
|
||||
const ACTION_GET_VOD_CATEGORIES: &str = "get_vod_categories";
|
||||
const ACTION_GET_SERIES_CATEGORIES: &str = "get_series_categories";
|
||||
const ACTION_GET_SERIES_CATEGORIES: &str = "get_series_categories";
|
||||
const ACTION_GET_LIVE_STREAMS: &str = "get_live_streams";
|
||||
const ACTION_GET_VOD_STREAMS: &str = "get_vod_streams";
|
||||
const ACTION_GET_SERIES: &str = "get_series";
|
||||
@@ -120,10 +122,10 @@ struct XtreamApiStreamRequest<'a> {
|
||||
|
||||
impl<'a> XtreamApiStreamRequest<'a> {
|
||||
pub const fn from(context: XtreamApiStreamContext,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
stream_id: &'a str,
|
||||
action_path: &'a str) -> Self {
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
stream_id: &'a str,
|
||||
action_path: &'a str) -> Self {
|
||||
Self {
|
||||
context,
|
||||
username,
|
||||
@@ -205,7 +207,7 @@ async fn xtream_player_api_stream(
|
||||
}
|
||||
let (action_stream_id, stream_ext) = xtream_api_request_separate_number_and_rest(stream_req.stream_id);
|
||||
let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse());
|
||||
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).await, true, format!("Failed to read xtream item for stream id {}", virtual_id));
|
||||
let input = try_option_bad_request!(app_state.config.get_input_by_id(pli.input_id), true, format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context));
|
||||
|
||||
if pli.item_type == PlaylistItemType::LiveHls {
|
||||
@@ -229,11 +231,11 @@ async fn xtream_player_api_stream(
|
||||
return HttpResponse::Found().insert_header(("Location", mask_sensitive_info(pli.url.as_str()))).finish();
|
||||
}
|
||||
|
||||
let stream_url = try_option_bad_request!(get_xtream_player_api_stream_url(input, stream_req.context.to_string().as_str(), &query_path, pli.url.as_str()), true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context));
|
||||
let stream_url = try_option_bad_request!(get_xtream_player_api_stream_url(input, stream_req.context.to_string().as_str(), &query_path, pli.url.as_str()), true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context));
|
||||
if log_enabled!(Level::Debug) {
|
||||
debug!("Streaming stream request from {}", mask_sensitive_info(&stream_url));
|
||||
}
|
||||
stream_response(&stream_url, req, Some(input)).await
|
||||
stream_response(app_state, &stream_url, req, Some(input)).await
|
||||
}
|
||||
|
||||
|
||||
@@ -331,7 +333,7 @@ async fn xtream_get_stream_info_content(info_url: &str, input: &ConfigInput) ->
|
||||
async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &ConfigTarget,
|
||||
pli: &XtreamPlaylistItem, info_url: &str, cluster: XtreamCluster) -> Result<String, Error> {
|
||||
if cluster == XtreamCluster::Series {
|
||||
if let Some(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.virtual_id) {
|
||||
if let Some(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.virtual_id).await {
|
||||
return Ok(content);
|
||||
}
|
||||
}
|
||||
@@ -340,12 +342,12 @@ async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &C
|
||||
return match cluster {
|
||||
XtreamCluster::Live => Ok(content),
|
||||
XtreamCluster::Video => get_xtream_vod_info(target, pli, &content),
|
||||
XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, pli, &content),
|
||||
XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, pli, &content).await,
|
||||
};
|
||||
}
|
||||
|
||||
Err(Error::new(ErrorKind::Other, format!("Cant find stream with id: {}/{}/{}",
|
||||
target.name.replace(' ', "_").as_str(), &cluster, pli.virtual_id)))
|
||||
target.name.replace(' ', "_").as_str(), &cluster, pli.virtual_id)))
|
||||
}
|
||||
|
||||
async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserCredentials,
|
||||
@@ -356,7 +358,7 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC
|
||||
Err(_) => return HttpResponse::BadRequest().finish()
|
||||
};
|
||||
|
||||
if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(cluster)) {
|
||||
if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(cluster)).await {
|
||||
let input_id = pli.input_id;
|
||||
if let Some(input) = app_state.config.get_input_by_id(input_id) {
|
||||
if let Some(info_url) = get_xtream_player_api_info_url(input, cluster, pli.provider_id) {
|
||||
@@ -384,7 +386,7 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials,
|
||||
Err(_) => return HttpResponse::BadRequest().finish()
|
||||
};
|
||||
|
||||
if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None) {
|
||||
if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None).await {
|
||||
let input_id: u16 = pli.input_id;
|
||||
if let Some(input) = app_state.config.get_input_by_id(input_id) {
|
||||
if let Some(action_url) = get_xtream_player_api_action_url(input, ACTION_GET_SHORT_EPG) {
|
||||
@@ -434,7 +436,7 @@ async fn xtream_player_api_handle_content_action(config: &Config, target_name: &
|
||||
|
||||
async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget, stream_id: &str, start: &str, end: &str) -> HttpResponse {
|
||||
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 pli = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(XtreamCluster::Live)).await);
|
||||
let input = try_option_bad_request!(app_state.config.get_input_by_id(pli.input_id));
|
||||
let info_url = try_option_bad_request!(get_xtream_player_api_action_url(input, ACTION_GET_CATCHUP_TABLE).map(|action_url| format!("{action_url}&{TAG_STREAM_ID}={}&start={start}&end={end}", pli.provider_id)));
|
||||
let content = try_result_bad_request!(xtream_get_stream_info_content(info_url.as_str(), input).await);
|
||||
@@ -465,7 +467,7 @@ async fn xtream_player_api(
|
||||
app_state: &web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
let user_target = get_user_target(&api_req, app_state);
|
||||
if let Some((user, target)) = user_target {
|
||||
if let Some((user, target)) = user_target {
|
||||
if !target.has_output(&TargetType::Xtream) {
|
||||
return HttpResponse::Ok().json(get_user_info(&user, &app_state.config));
|
||||
}
|
||||
@@ -478,22 +480,22 @@ async fn xtream_player_api(
|
||||
match action {
|
||||
ACTION_GET_SERIES_INFO => {
|
||||
return xtream_get_stream_info_response(
|
||||
app_state, &user, target, api_req.series_id.trim(), XtreamCluster::Series
|
||||
app_state, &user, target, api_req.series_id.trim(), XtreamCluster::Series,
|
||||
).await;
|
||||
}
|
||||
ACTION_GET_VOD_INFO => {
|
||||
return xtream_get_stream_info_response(
|
||||
app_state, &user, target, api_req.vod_id.trim(), XtreamCluster::Video
|
||||
app_state, &user, target, api_req.vod_id.trim(), XtreamCluster::Video,
|
||||
).await;
|
||||
}
|
||||
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()
|
||||
app_state, &user, target, api_req.stream_id.trim(), api_req.limit.trim(),
|
||||
).await;
|
||||
}
|
||||
ACTION_GET_CATCHUP_TABLE => {
|
||||
return xtream_get_catchup_response(
|
||||
app_state, target, api_req.stream_id.trim(), api_req.start.trim(), api_req.end.trim()
|
||||
app_state, target, api_req.stream_id.trim(), api_req.start.trim(), api_req.end.trim(),
|
||||
).await;
|
||||
}
|
||||
_ => {}
|
||||
@@ -501,7 +503,7 @@ async fn xtream_player_api(
|
||||
|
||||
// Handle general content actions
|
||||
if let Some(response) = xtream_player_api_handle_content_action(
|
||||
&app_state.config, &target.name, action, api_req.category_id.trim(), req
|
||||
&app_state.config, &target.name, action, api_req.category_id.trim(), req,
|
||||
).await {
|
||||
return response;
|
||||
}
|
||||
@@ -510,15 +512,15 @@ async fn xtream_player_api(
|
||||
let category_id = api_req.category_id.trim().parse::<u32>().unwrap_or(0);
|
||||
let result = match action {
|
||||
ACTION_GET_LIVE_STREAMS => xtream_repository::xtream_load_rewrite_playlist(
|
||||
XtreamCluster::Live, &app_state.config, target, category_id
|
||||
),
|
||||
XtreamCluster::Live, &app_state.config, target, category_id,
|
||||
).await,
|
||||
ACTION_GET_VOD_STREAMS => xtream_repository::xtream_load_rewrite_playlist(
|
||||
XtreamCluster::Video, &app_state.config, target, category_id
|
||||
),
|
||||
XtreamCluster::Video, &app_state.config, target, category_id,
|
||||
).await,
|
||||
ACTION_GET_SERIES => xtream_repository::xtream_load_rewrite_playlist(
|
||||
XtreamCluster::Series, &app_state.config, target, category_id
|
||||
),
|
||||
_ => Err(M3uFilterError::new(M3uFilterErrorKind::Info,format!("Cant find action: {action} for target: {}", &target.name)
|
||||
XtreamCluster::Series, &app_state.config, target, category_id,
|
||||
).await,
|
||||
_ => Err(M3uFilterError::new(M3uFilterErrorKind::Info, format!("Cant find action: {action} for target: {}", &target.name),
|
||||
)),
|
||||
};
|
||||
|
||||
@@ -547,7 +549,7 @@ async fn xtream_player_api(
|
||||
}
|
||||
}
|
||||
|
||||
fn xtream_create_content_stream(xtream_iter: impl Iterator<Item = String>) -> impl Stream<Item = Result<Bytes, String>> {
|
||||
fn xtream_create_content_stream(xtream_iter: impl Iterator<Item=String>) -> impl Stream<Item=Result<Bytes, String>> {
|
||||
let mut first_item = true;
|
||||
stream::iter(xtream_iter.map(move |line| {
|
||||
let line = if first_item {
|
||||
|
||||
Reference in New Issue
Block a user