mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-31 11:12:36 +01:00
File download and web-ui refactoring
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use actix_web::web;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::model::config::{Config, ConfigOptions, ConfigRename, ConfigSort, InputType, ProcessTargets, TargetOutput, VideoConfig};
|
||||
@@ -7,6 +8,7 @@ use crate::model::model_config::{default_as_empty_str, ProcessingOrder};
|
||||
pub(crate) struct AppState {
|
||||
pub config: Arc<Config>,
|
||||
pub targets: Arc<ProcessTargets>,
|
||||
pub downloads: Arc<Mutex<HashMap<String, u64>>>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
+3
-1
@@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_cors::Cors;
|
||||
@@ -44,6 +45,7 @@ pub(crate) async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>)
|
||||
let shared_data = web::Data::new(AppState {
|
||||
config: cfg,
|
||||
targets,
|
||||
downloads: Arc::new(Mutex::new(HashMap::new()))
|
||||
});
|
||||
|
||||
// Scheduler
|
||||
|
||||
+76
-33
@@ -1,16 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::{fs, io};
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use actix_web::{HttpResponse, Scope, web};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
use crate::api::api_model::{AppState, FileDownloadRequest, PlaylistRequest, ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig};
|
||||
use crate::download::{get_m3u_playlist, get_xtream_playlist};
|
||||
use crate::model::config::{ConfigInput, InputType};
|
||||
use crate::utils::{bytes_to_megabytes};
|
||||
use futures::stream::TryStreamExt;
|
||||
use log::error;
|
||||
use log::{error, info};
|
||||
use reqwest::{header, Response};
|
||||
use reqwest::header::{HeaderName, HeaderValue};
|
||||
use unidecode::unidecode;
|
||||
|
||||
const DOWNLOAD_HEADERS: &[(&str, &str)] = &[
|
||||
("Accept", "video/*"),
|
||||
@@ -97,12 +102,13 @@ pub(crate) async fn config(
|
||||
HttpResponse::Ok().json(result)
|
||||
}
|
||||
|
||||
async fn async_download_file(filename: &str, response: Response) -> Result<(), String> {
|
||||
let path = PathBuf::from(filename);
|
||||
async fn async_download_file(download_id: &String, path: &PathBuf, response: Response, downloads: Arc<Mutex<HashMap<String, u64>>>) -> Result<u64, String> {
|
||||
match File::create(path) {
|
||||
Ok(mut file) => {
|
||||
info!("Downloading {}", path.to_str().unwrap_or("?"));
|
||||
let mut stream = response.bytes_stream().map_err(|err| io::Error::new(ErrorKind::Other, err));
|
||||
let mut downloaded: u64 = 0;
|
||||
downloads.lock().unwrap().insert(download_id.to_owned(), downloaded);
|
||||
loop {
|
||||
match stream.try_next().await {
|
||||
Ok(item) => {
|
||||
@@ -112,11 +118,16 @@ async fn async_download_file(filename: &str, response: Response) -> Result<(), S
|
||||
Ok(_) => {
|
||||
let new = downloaded + (chunk.len() as u64);
|
||||
downloaded = new;
|
||||
downloads.lock().unwrap().insert(download_id.to_owned(), downloaded);
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {}", err))
|
||||
}
|
||||
}
|
||||
None => return Ok(()),
|
||||
None => {
|
||||
let megabytes = bytes_to_megabytes(downloaded);
|
||||
info!("Downloaded {}, filesize: {}MB", path.to_str().unwrap_or("?"), megabytes);
|
||||
return Ok(downloaded)
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {}", err)),
|
||||
@@ -127,39 +138,70 @@ async fn async_download_file(filename: &str, response: Response) -> Result<(), S
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn download_file_info(
|
||||
info: web::Path<String>,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
let did: String = info.into_inner();
|
||||
match _app_state.downloads.lock().unwrap().get(&did) {
|
||||
// @TODO it is only a success when the file remains.
|
||||
None => HttpResponse::Ok().json(json!({"download_id": &did, "finished": true})),
|
||||
Some(downloaded) => HttpResponse::Ok().json(json!({"download_id": &did, "filesize": downloaded}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn download_file(
|
||||
req: web::Json<FileDownloadRequest>,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
match reqwest::Url::parse(&req.url) {
|
||||
Ok(_url) => {
|
||||
let client = reqwest::Client::new();
|
||||
let mut headers = header::HeaderMap::new();
|
||||
for (key, value) in DOWNLOAD_HEADERS {
|
||||
headers.insert(
|
||||
HeaderName::from_bytes(key.as_bytes()).unwrap(),
|
||||
HeaderValue::from_bytes(value.as_bytes()).unwrap(),
|
||||
);
|
||||
}
|
||||
match client.get(_url).headers(headers).send().await {
|
||||
Ok(response) => {
|
||||
actix_rt::spawn(async move {
|
||||
match async_download_file(&req.filename, response).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => error!("{}", err)
|
||||
}
|
||||
});
|
||||
HttpResponse::Ok().json(json!({"success": true}))
|
||||
}
|
||||
Err(err) => HttpResponse::InternalServerError().json(json!({"error": format!("{}", err)})),
|
||||
}
|
||||
|
||||
//
|
||||
// use rocket::futures::TryStreamExt; // for map_err() call below:
|
||||
// let reader = StreamReader::new(response.bytes_stream().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
|
||||
// rocket::response::Stream::chunked(reader, 4096)
|
||||
if let Some(download) = &_app_state.config.video.as_ref().unwrap().download {
|
||||
if download.directory.is_none() {
|
||||
return HttpResponse::BadRequest().json(json!({"error": "Server config missing video.download.directory configuration"}));
|
||||
}
|
||||
Err(_) => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})),
|
||||
|
||||
match reqwest::Url::parse(&req.url) {
|
||||
Ok(_url) => {
|
||||
let client = reqwest::Client::new();
|
||||
let mut headers = header::HeaderMap::new();
|
||||
for (key, value) in DOWNLOAD_HEADERS {
|
||||
headers.insert(
|
||||
HeaderName::from_bytes(key.as_bytes()).unwrap(),
|
||||
HeaderValue::from_bytes(value.as_bytes()).unwrap(),
|
||||
);
|
||||
}
|
||||
match client.get(_url).headers(headers).send().await {
|
||||
Ok(response) => {
|
||||
let filename = unidecode(&req.filename).replace(' ', "_");
|
||||
let path: PathBuf = [(download.directory.clone().unwrap().as_str()), filename.as_str()].iter().collect();
|
||||
let download_id = Uuid::new_v4().to_string();
|
||||
let response_download_id = download_id.clone();
|
||||
actix_rt::spawn(async move {
|
||||
let downloads = _app_state.downloads.clone();
|
||||
match async_download_file(&download_id, &path, response, downloads.clone()).await {
|
||||
Ok(_) => {
|
||||
downloads.lock().unwrap().remove(&download_id);
|
||||
}
|
||||
Err(err) => {
|
||||
downloads.lock().unwrap().remove(&download_id);
|
||||
let _ = fs::remove_file(&path);
|
||||
error!("{}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
HttpResponse::Ok().json(json!({"download_id": response_download_id}))
|
||||
}
|
||||
Err(err) => HttpResponse::InternalServerError().json(json!({"error": format!("{}", err)})),
|
||||
}
|
||||
|
||||
//
|
||||
// use rocket::futures::TryStreamExt; // for map_err() call below:
|
||||
// let reader = StreamReader::new(response.bytes_stream().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
|
||||
// rocket::response::Stream::chunked(reader, 4096)
|
||||
}
|
||||
Err(_) => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest().json(json!({"error": "Server config missing video.download configuration"}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,4 +210,5 @@ pub(crate) fn v1_api_register() -> Scope {
|
||||
.route("/config", web::get().to(config))
|
||||
.route("/playlist", web::post().to(playlist))
|
||||
.route("/file/download", web::post().to(download_file))
|
||||
.route("/file/download/{did}", web::get().to(download_file_info))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user