Download Queue added

This commit is contained in:
euzu
2023-11-06 21:33:48 +01:00
parent 075ec974e8
commit 8a984aa79d
10 changed files with 245 additions and 190 deletions
+3 -1
View File
@@ -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.
+22 -24
View File
@@ -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>;
}
+8 -2
View File
@@ -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[];
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}