mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-18 22:03:01 +02:00
Stream sharing for live tv
This commit is contained in:
@@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user