mirror of
https://github.com/euzu/tuliprox.git
synced 2026-09-28 20:12:12 +02:00
Download Queue added
This commit is contained in:
@@ -241,7 +241,9 @@ Example filter: `((Group ~ "^DE.*") AND (NOT Title ~ ".*Shopping.*")) OR (Group
|
||||
If you use characters like `+ | [ ] ( )` in filters don't forget to escape them!!
|
||||
|
||||
The regular expression syntax is similar to Perl-style regular expressions,
|
||||
but lacks a few features like look around and backreferences.
|
||||
but lacks a few features like look around and backreferences.
|
||||
To test the regular expression i use [regex101.com](https://regex101.com/).
|
||||
Don't forget to select `Rust` option which is under the `FLAVOR` section on the left.
|
||||
|
||||
### 1.5.2.6 `rename`
|
||||
Is a List of rename configurations. Each configuration has 3 top level entries.
|
||||
|
||||
@@ -5,10 +5,11 @@ import {first} from "rxjs/operators";
|
||||
|
||||
//const FILE_API_PATH = 'file';
|
||||
const FILE_DOWNLOAD_API_PATH = 'file/download';
|
||||
const FILE_DOWNLOAD_INFO_API_PATH =FILE_DOWNLOAD_API_PATH + '/info';
|
||||
|
||||
export default interface FileApiService extends ApiService {
|
||||
download(req: FileDownloadRequest): Observable<FileDownloadResponse>;
|
||||
getDownloadInfo(download_id: string): Observable<FileDownloadInfo>;
|
||||
getDownloadInfo(): Observable<FileDownloadInfo>;
|
||||
}
|
||||
|
||||
export class DefaultFileApiService extends DefaultApiService implements FileApiService {
|
||||
@@ -19,28 +20,25 @@ export class DefaultFileApiService extends DefaultApiService implements FileApiS
|
||||
return throwError(() => new Error('Invalid arguments'));
|
||||
}
|
||||
|
||||
getDownloadInfo(download_id: string): Observable<FileDownloadInfo> {
|
||||
if (download_id) {
|
||||
return new Observable((observer) => {
|
||||
const fetch_info = () => {
|
||||
this.get<FileDownloadInfo>(FILE_DOWNLOAD_API_PATH + '/' + download_id).pipe(first()).subscribe({
|
||||
next: (info: FileDownloadInfo) => {
|
||||
if (info.finished) {
|
||||
observer.next(info);
|
||||
observer.complete();
|
||||
} else if (info.filesize != undefined) {
|
||||
observer.next(info);
|
||||
setTimeout(() => fetch_info(), 1000);
|
||||
} else {
|
||||
observer.error("unknown file download state");
|
||||
}
|
||||
},
|
||||
error: (err) => observer.error(err)
|
||||
});
|
||||
}
|
||||
fetch_info();
|
||||
});
|
||||
}
|
||||
return throwError(() => new Error('Invalid arguments'));
|
||||
getDownloadInfo(): Observable<FileDownloadInfo> {
|
||||
return new Observable((observer) => {
|
||||
const fetch_info = () => {
|
||||
this.get<FileDownloadInfo>(FILE_DOWNLOAD_INFO_API_PATH).pipe(first()).subscribe({
|
||||
next: (info: FileDownloadInfo) => {
|
||||
if (info.finished) {
|
||||
observer.next(info);
|
||||
observer.complete();
|
||||
} else if (info.filesize != undefined) {
|
||||
observer.next(info);
|
||||
setTimeout(() => fetch_info(), 1000);
|
||||
} else {
|
||||
observer.error("unknown file download state");
|
||||
}
|
||||
},
|
||||
error: (err) => observer.error(err),
|
||||
});
|
||||
}
|
||||
fetch_info();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use '../../scss/common';
|
||||
|
||||
.playlist-tree {
|
||||
font-size: 12px;
|
||||
font-size: 1rem;
|
||||
|
||||
.tree-expander {
|
||||
margin-right: 6px;
|
||||
@@ -24,7 +24,7 @@
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
height: 1.3em;
|
||||
height: 1.3rem;
|
||||
|
||||
.tree-group-header-content {
|
||||
display: flex;
|
||||
@@ -49,7 +49,8 @@
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
margin-left: 50px;
|
||||
height: 1.3em;
|
||||
height: 1.3rem;
|
||||
gap: 3px;
|
||||
|
||||
.tree-channel-tools {
|
||||
display: flex;
|
||||
@@ -61,15 +62,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
height: 1.1rem;
|
||||
width: 1.1rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--tool-button-background-color);
|
||||
color: var(--tool-button-color);
|
||||
|
||||
svg {
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -87,8 +88,8 @@
|
||||
}
|
||||
|
||||
.tree-channel-nr {
|
||||
width: 20px;
|
||||
font-size: 0.8em;
|
||||
min-width: 2rem;
|
||||
font-size: 1rem;
|
||||
margin-right: 8px;
|
||||
text-align: right;
|
||||
color: var(--tree-count-color);
|
||||
|
||||
@@ -12,8 +12,6 @@ import {FileDownloadInfo, FileDownloadResponse} from "../../model/file-download"
|
||||
|
||||
const VALID_VIDEO_FILES = ['mkv', 'mp4', 'avi'];
|
||||
|
||||
type DownloadInfo = {filename: string, filesize: number};
|
||||
|
||||
export type PlaylistTreeState = { [key: number]: boolean };
|
||||
|
||||
interface PlaylistTreeProps {
|
||||
@@ -32,7 +30,8 @@ export default function PlaylistTree(props: PlaylistTreeProps) {
|
||||
const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar();
|
||||
const services = useServices();
|
||||
const [videoExtensions, setVideoExtensions] = useState<string[]>([]);
|
||||
const [downloads, setDownloads] = useState<Record<string, DownloadInfo>>({})
|
||||
const [download, setDownload] = useState<FileDownloadInfo>(undefined)
|
||||
const downloading = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverConfig) {
|
||||
@@ -41,28 +40,6 @@ export default function PlaylistTree(props: PlaylistTreeProps) {
|
||||
return noop;
|
||||
}, [serverConfig]);
|
||||
|
||||
|
||||
const setDownloadsInfo = useCallback((info: FileDownloadInfo) => {
|
||||
if (info.finished == undefined && info.filesize == undefined) {
|
||||
setDownloads((downloads) => {
|
||||
downloads[info.download_id] = {filename: info.filename, filesize: 0};
|
||||
return {...downloads};
|
||||
});
|
||||
} else {
|
||||
if (info.filesize != undefined) {
|
||||
setDownloads((downloads) => {
|
||||
downloads[info.download_id].filesize = info.filesize;
|
||||
return {...downloads};
|
||||
});
|
||||
} else {
|
||||
setDownloads((downloads) => {
|
||||
delete downloads[info.download_id];
|
||||
return {...downloads};
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPlaylistItemById = useCallback((itemId: string): PlaylistItem => {
|
||||
const id = parseInt(itemId);
|
||||
if (data && !isNaN(id)) {
|
||||
@@ -103,16 +80,28 @@ export default function PlaylistTree(props: PlaylistTreeProps) {
|
||||
}
|
||||
}, [enqueueSnackbar, getPlaylistItemById, onCopy]);
|
||||
|
||||
const startPollingDownload = useCallback((downloadId: string) => {
|
||||
let subs: Subscription = services.file().getDownloadInfo(downloadId).subscribe({
|
||||
next: (info: FileDownloadInfo) => setDownloadsInfo(info),
|
||||
error: (err) => enqueueSnackbar("Download file failed!", {variant: 'error'}),
|
||||
complete: () => subs.unsubscribe()
|
||||
const startPollingDownload = useCallback(() => {
|
||||
let subs: Subscription = services.file().getDownloadInfo().subscribe({
|
||||
next: (info: FileDownloadInfo) => {
|
||||
if (info.finished === true) {
|
||||
setDownload(undefined);
|
||||
} else {
|
||||
setDownload(info);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
enqueueSnackbar("Download file failed!", {variant: 'error'});
|
||||
setDownload(undefined);
|
||||
},
|
||||
complete: () => {
|
||||
subs.unsubscribe();
|
||||
setDownload(undefined);
|
||||
}
|
||||
});
|
||||
}, [setDownloadsInfo, enqueueSnackbar, services]);
|
||||
}, [setDownload, enqueueSnackbar, services]);
|
||||
|
||||
const handleDownloadUrl = useCallback((e: any) => {
|
||||
if (! serverConfig.video.download?.directory) {
|
||||
if (!serverConfig.video.download?.directory) {
|
||||
enqueueSnackbar("Please updated the server configuration and add video.download directory and headers!", {variant: 'error'})
|
||||
} else {
|
||||
const item = getPlaylistItemById(e.target.dataset.item);
|
||||
@@ -125,11 +114,14 @@ export default function PlaylistTree(props: PlaylistTreeProps) {
|
||||
}
|
||||
|
||||
if (VALID_VIDEO_FILES.includes(ext)) {
|
||||
filename = filename + '.' + ext;
|
||||
services.file().download({url: item.header.url, filename}).pipe(first()).subscribe({
|
||||
services.file().download({
|
||||
url: item.header.url,
|
||||
filename: filename + '.' + ext
|
||||
}).pipe(first()).subscribe({
|
||||
next: (download: FileDownloadResponse) => {
|
||||
setDownloadsInfo({download_id: download.download_id, filename: filename});
|
||||
startPollingDownload(download.download_id)
|
||||
if (download.success) {
|
||||
startPollingDownload();
|
||||
}
|
||||
},
|
||||
error: err => enqueueSnackbar("Download failed!", {variant: 'error'}),
|
||||
complete: noop,
|
||||
@@ -139,7 +131,7 @@ export default function PlaylistTree(props: PlaylistTreeProps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [serverConfig, enqueueSnackbar, getPlaylistItemById, services, startPollingDownload, setDownloadsInfo]);
|
||||
}, [serverConfig, enqueueSnackbar, getPlaylistItemById, services, startPollingDownload]);
|
||||
|
||||
const handlePlayUrl = useCallback((e: any) => {
|
||||
if (onPlay) {
|
||||
@@ -151,14 +143,14 @@ export default function PlaylistTree(props: PlaylistTreeProps) {
|
||||
}, [onPlay, getPlaylistItemById]);
|
||||
|
||||
const isVideoFile = useCallback((entry: PlaylistItem): boolean => {
|
||||
if (videoExtensions && entry.header.url) {
|
||||
for (const ext of videoExtensions) {
|
||||
if (entry.header.url.endsWith(ext)) {
|
||||
return true;
|
||||
}
|
||||
if (videoExtensions && entry.header.url) {
|
||||
for (const ext of videoExtensions) {
|
||||
if (entry.header.url.endsWith(ext)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}, [videoExtensions]);
|
||||
|
||||
const renderEntry = useCallback((entry: PlaylistItem, index: number): React.ReactNode => {
|
||||
@@ -211,16 +203,16 @@ export default function PlaylistTree(props: PlaylistTreeProps) {
|
||||
}, [data, renderGroup]);
|
||||
|
||||
const renderDownloads = useCallback((): React.ReactNode => {
|
||||
const keys = Object.keys(downloads)
|
||||
if (keys.length) {
|
||||
let elements = keys.map(key => {
|
||||
const info: DownloadInfo = downloads[key];
|
||||
return <li key={key}>{info.filename}: {info.filesize ? (info.filesize / 1_048_576).toFixed(2) : 0} MB</li>;
|
||||
})
|
||||
return <div className={'download-info'}><ul>{elements}</ul></div>;
|
||||
if (download) {
|
||||
return <div className={'download-info'}>
|
||||
<ul>
|
||||
<li>{download.filename}: {download.filesize ? (download.filesize / 1_048_576).toFixed(2) : 0} MB</li>
|
||||
{download.errors?.length && <li>{download.errors}</li>}
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
return <></>;
|
||||
}, [downloads]);
|
||||
}, [download]);
|
||||
|
||||
return <div className={'playlist-tree'}>{renderPlaylist()}{renderDownloads()}</div>;
|
||||
}
|
||||
@@ -4,12 +4,18 @@ export interface FileDownloadRequest {
|
||||
}
|
||||
|
||||
export interface FileDownloadResponse {
|
||||
download_id: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadErrorInfo {
|
||||
filename: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
|
||||
export interface FileDownloadInfo {
|
||||
download_id: string;
|
||||
filename?: string;
|
||||
finished?: boolean;
|
||||
filesize?: number;
|
||||
errors?: DownloadErrorInfo[];
|
||||
}
|
||||
@@ -24,8 +24,8 @@ export default class FileService {
|
||||
return this.fileApiService.download(req);
|
||||
}
|
||||
|
||||
getDownloadInfo(download_id: string): Observable<FileDownloadInfo> {
|
||||
return this.fileApiService.getDownloadInfo(download_id);
|
||||
getDownloadInfo(): Observable<FileDownloadInfo> {
|
||||
return this.fileApiService.getDownloadInfo();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-3
@@ -1,15 +1,37 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::path::{PathBuf};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use actix_web::web;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::model::api_proxy::{ApiProxyConfig};
|
||||
use crate::model::config::{Config, ConfigTargetOptions, ConfigRename, ConfigSort, InputType, ProcessTargets, TargetOutput, VideoConfig};
|
||||
use crate::model::model_config::{default_as_empty_str, ProcessingOrder};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct DownloadErrorInfo {
|
||||
pub filename: String,
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FileDownload {
|
||||
pub file_dir: PathBuf,
|
||||
pub file_path: PathBuf,
|
||||
pub filename: String,
|
||||
pub url: reqwest::Url,
|
||||
pub size: u64,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct DownloadQueue {
|
||||
pub queue: Arc<Mutex<Vec<FileDownload>>>,
|
||||
pub active: Arc<RwLock<Option<FileDownload>>>,
|
||||
pub errors: Arc<RwLock<Vec<FileDownload>>>,
|
||||
}
|
||||
|
||||
pub(crate) struct AppState {
|
||||
pub config: Arc<Config>,
|
||||
pub targets: Arc<ProcessTargets>,
|
||||
pub downloads: Arc<Mutex<HashMap<String, u64>>>
|
||||
pub downloads: Arc<DownloadQueue>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
+7
-4
@@ -1,7 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_files::NamedFile;
|
||||
@@ -9,7 +8,7 @@ use actix_web::{App, get, HttpRequest, HttpServer, web};
|
||||
use actix_web::middleware::Logger;
|
||||
use crate::api::m3u_api::{m3u_api_register};
|
||||
|
||||
use crate::api::api_model::{AppState};
|
||||
use crate::api::api_model::{AppState, DownloadQueue};
|
||||
use crate::api::scheduler::start_scheduler;
|
||||
use crate::api::v1_api::{v1_api_register};
|
||||
use crate::api::xmltv_api::{xmltv_api_register};
|
||||
@@ -41,7 +40,11 @@ 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()))
|
||||
downloads: Arc::from(DownloadQueue {
|
||||
queue: Arc::from(Mutex::new(Vec::new())),
|
||||
active: Arc::from(RwLock::new(None)),
|
||||
errors: Arc::from(RwLock::new(Vec::new())),
|
||||
})
|
||||
});
|
||||
|
||||
// Scheduler
|
||||
|
||||
+109
-84
@@ -1,21 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::{fs, io};
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, RwLock};
|
||||
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 serde_json::{json};
|
||||
use crate::api::api_model::{AppState, DownloadErrorInfo, DownloadQueue, FileDownload, FileDownloadRequest, PlaylistRequest, ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig};
|
||||
use crate::download::{get_m3u_playlist, get_xtream_playlist};
|
||||
use crate::model::config::{ConfigInput, InputType, validate_targets, VideoDownloadConfig};
|
||||
use crate::utils::{bytes_to_megabytes};
|
||||
use crate::utils::{bytes_to_megabytes, get_request_headers};
|
||||
use futures::stream::TryStreamExt;
|
||||
use log::{error, info};
|
||||
use reqwest::{header, Response};
|
||||
use reqwest::header::{HeaderName, HeaderValue};
|
||||
use reqwest::header::HeaderMap;
|
||||
use unidecode::unidecode;
|
||||
use crate::config_reader::save_api_proxy;
|
||||
use crate::m3u_filter_error::M3uFilterError;
|
||||
@@ -169,55 +167,106 @@ pub(crate) async fn config(
|
||||
HttpResponse::Ok().json(result)
|
||||
}
|
||||
|
||||
async fn async_download_file(download_id: &String, path: &Path, 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) => {
|
||||
match item {
|
||||
Some(chunk) => {
|
||||
match file.write_all(&chunk) {
|
||||
Ok(_) => {
|
||||
let new = downloaded + (chunk.len() as u64);
|
||||
downloaded = new;
|
||||
downloads.lock().unwrap().insert(download_id.to_owned(), downloaded);
|
||||
async fn download_file(active: Arc<RwLock<Option<FileDownload>>>, headers: HeaderMap) -> Result<(), String> {
|
||||
let client = reqwest::Client::new();
|
||||
let file_download = active.read().unwrap().as_ref().unwrap().clone();
|
||||
match client.get(file_download.url.clone()).headers(headers).send().await {
|
||||
Ok(response) => {
|
||||
match fs::create_dir_all(&file_download.file_dir) {
|
||||
Ok(_) => {
|
||||
let file_path_str = file_download.file_path.to_str().unwrap_or("?");
|
||||
match File::create(&file_download.file_path) {
|
||||
Ok(mut file) => {
|
||||
info!("Downloading {}", file_download.file_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;
|
||||
loop {
|
||||
match stream.try_next().await {
|
||||
Ok(item) => {
|
||||
match item {
|
||||
Some(chunk) => {
|
||||
match file.write_all(&chunk) {
|
||||
Ok(_) => {
|
||||
downloaded += (chunk.len() as u64);
|
||||
active.write().unwrap().as_mut().unwrap().size = downloaded;
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {} {}", file_path_str, err))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let megabytes = bytes_to_megabytes(downloaded);
|
||||
info!("Downloaded {}, filesize: {}MB", file_path_str, megabytes);
|
||||
active.write().unwrap().as_mut().unwrap().size = downloaded;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {} {}", path.to_str().unwrap_or("?"), err))
|
||||
Err(err) => return Err(format!("Error while writing to file: {} {}", file_path_str, err))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let megabytes = bytes_to_megabytes(downloaded);
|
||||
info!("Downloaded {}, filesize: {}MB", path.to_str().unwrap_or("?"), megabytes);
|
||||
return Ok(downloaded);
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("Error while writing to file: {} {}", file_path_str, err))
|
||||
}
|
||||
Err(err) => return Err(format!("Error while writing to file: {} {}", path.to_str().unwrap_or("?"), err))
|
||||
}
|
||||
Err(err) => Err(format!("Error while creating directory to file: {} {}", &file_download.file_dir.to_str().unwrap_or("?"), err))
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("Error while writing to file: {} {}", path.to_str().unwrap_or("?"), err))
|
||||
Err(err) => Err(format!("Error while opening url: {} {}", &file_download.url, err))
|
||||
}
|
||||
}
|
||||
|
||||
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}))
|
||||
let error_list: &[DownloadErrorInfo] = &_app_state.downloads.errors.write().unwrap().drain(..)
|
||||
.map(|e| DownloadErrorInfo { filename: e.filename, error: e.error.unwrap() }).collect::<Vec<DownloadErrorInfo>>();
|
||||
let errors = match serde_json::to_string(error_list) {
|
||||
Ok(value) => value,
|
||||
Err(_) => "[]".to_string()
|
||||
};
|
||||
match &*_app_state.downloads.active.read().unwrap() {
|
||||
None => HttpResponse::Ok().json(json!({"finished": true, "errors": errors})),
|
||||
Some(file_download) =>
|
||||
HttpResponse::Ok().json(json!({"filename": file_download.filename, "filesize": file_download.size, "errors": errors}))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn download_file(
|
||||
fn run_download_queue(download_cfg: &VideoDownloadConfig, download_queue: Arc<DownloadQueue>) {
|
||||
if let Some(file_download) = download_queue.as_ref().queue.lock().unwrap().pop() {
|
||||
*download_queue.as_ref().active.write().unwrap() = Some(file_download);
|
||||
let headers = get_request_headers(&download_cfg.headers);
|
||||
let dq = Arc::clone(&download_queue);
|
||||
actix_rt::spawn(async move {
|
||||
loop {
|
||||
let opt: Option<FileDownload> = {
|
||||
dq.active.read().unwrap().deref().clone()
|
||||
};
|
||||
match opt {
|
||||
Some(_) => {
|
||||
match download_file(Arc::clone(&dq.active), headers.clone()).await {
|
||||
Ok(_) => {
|
||||
*dq.active.write().unwrap() = dq.queue.lock().unwrap().pop();
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(fd) = &mut *dq.active.write().unwrap() {
|
||||
fd.error = Some(err);
|
||||
dq.errors.write().unwrap().push(fd.clone());
|
||||
}
|
||||
*dq.active.write().unwrap() = dq.queue.lock().unwrap().pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(crate) async fn queue_download_file(
|
||||
req: web::Json<FileDownloadRequest>,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
@@ -228,49 +277,25 @@ pub(crate) async fn download_file(
|
||||
|
||||
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(),
|
||||
);
|
||||
let filename_re = download._re_filename.as_ref().unwrap();
|
||||
let filename = filename_re.replace_all(&unidecode(&req.filename).replace(' ', "_"), "").to_string();
|
||||
let file_name = filename.clone();
|
||||
let file_dir = get_download_directory(download, &filename);
|
||||
let mut file_path: PathBuf = file_dir.clone();
|
||||
file_path.push(&filename);
|
||||
let file_download = FileDownload {
|
||||
file_dir,
|
||||
file_path,
|
||||
filename,
|
||||
url: _url,
|
||||
size: 0,
|
||||
error: None,
|
||||
};
|
||||
_app_state.downloads.queue.lock().unwrap().push(file_download);
|
||||
if _app_state.downloads.active.read().unwrap().is_none() {
|
||||
run_download_queue(download, Arc::clone(&_app_state.downloads));
|
||||
}
|
||||
match client.get(_url).headers(headers).send().await {
|
||||
Ok(response) => {
|
||||
let filename_re = download._re_filename.as_ref().unwrap();
|
||||
let filename = filename_re.replace_all(&unidecode(&req.filename).replace(' ', "_"), "").to_string();
|
||||
let file_dir = get_download_directory(download, &filename);
|
||||
match fs::create_dir_all(&file_dir) {
|
||||
Ok(_) => {
|
||||
let path = file_dir.join(filename.as_str());
|
||||
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)}))
|
||||
}
|
||||
}
|
||||
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)
|
||||
HttpResponse::Ok().json(json!({"success": file_name}))
|
||||
}
|
||||
Err(_) => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})),
|
||||
}
|
||||
@@ -307,6 +332,6 @@ pub(crate) fn v1_api_register() -> Scope {
|
||||
.route("/config/serverinfo", web::post().to(config_api_proxy_server_info))
|
||||
.route("/playlist", web::post().to(playlist))
|
||||
.route("/playlist/update", web::post().to(playlist_update))
|
||||
.route("/file/download", web::post().to(download_file))
|
||||
.route("/file/download/{did}", web::get().to(download_file_info))
|
||||
.route("/file/download", web::post().to(queue_download_file))
|
||||
.route("/file/download/info", web::get().to(download_file_info))
|
||||
}
|
||||
|
||||
+16
-10
@@ -1,10 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use log::{debug, error};
|
||||
use path_absolutize::*;
|
||||
use reqwest::header;
|
||||
use reqwest::header::{HeaderName, HeaderValue};
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use crate::create_m3u_filter_error_result;
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::model::config::{ConfigInput};
|
||||
@@ -198,19 +199,24 @@ pub(crate) fn get_file_path(wd: &String, path: Option<PathBuf>) -> Option<PathBu
|
||||
fn get_client_request(input: &ConfigInput, url: url::Url) -> reqwest::RequestBuilder {
|
||||
let mut request = reqwest::Client::new().get(url);
|
||||
if input.headers.is_empty() {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
for (key, value) in &input.headers {
|
||||
headers.insert(
|
||||
HeaderName::from_bytes(key.as_bytes()).unwrap(),
|
||||
HeaderValue::from_bytes(value.as_bytes()).unwrap(),
|
||||
);
|
||||
}
|
||||
debug!("Request with headers{:?}", &headers);
|
||||
let headers = get_request_headers(&input.headers);
|
||||
request = request.headers(headers);
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
pub fn get_request_headers(defined_headers: &HashMap<String, String>) -> HeaderMap {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
for (key, value) in defined_headers {
|
||||
headers.insert(
|
||||
HeaderName::from_bytes(key.as_bytes()).unwrap(),
|
||||
HeaderValue::from_bytes(value.as_bytes()).unwrap(),
|
||||
);
|
||||
}
|
||||
debug!("Request with headers{:?}", &headers);
|
||||
headers
|
||||
}
|
||||
|
||||
async fn download_json_content(input: &ConfigInput, url: url::Url, persist_filepath: Option<PathBuf>) -> Result<serde_json::Value, String> {
|
||||
let request = get_client_request(input, url);
|
||||
match request.send().await {
|
||||
@@ -284,4 +290,4 @@ pub(crate) fn path_exists(file_path: &Path) -> bool {
|
||||
return metadata.is_file()
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user