|
|
|
@ -1,13 +1,22 @@ |
|
|
|
mod download_error;
|
|
|
|
mod client;
|
|
|
|
|
|
|
|
use std::time::Duration;
|
|
|
|
use std::{
|
|
|
|
env::{current_dir, set_current_dir},
|
|
|
|
ffi::OsStr,
|
|
|
|
io::ErrorKind,
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
process::Command,
|
|
|
|
time::Duration
|
|
|
|
};
|
|
|
|
|
|
|
|
use anyhow::anyhow;
|
|
|
|
use clap::Parser;
|
|
|
|
use env_logger::Env;
|
|
|
|
use http::Uri;
|
|
|
|
use tokio::task::JoinSet;
|
|
|
|
use shellwords::escape;
|
|
|
|
use tokio::{fs::{create_dir, remove_dir_all}, task::JoinSet};
|
|
|
|
use which::which;
|
|
|
|
|
|
|
|
use log::{log, Level};
|
|
|
|
|
|
|
|
@ -21,6 +30,8 @@ struct DownloadMessage { |
|
|
|
|
|
|
|
#[derive(Debug, Parser)]
|
|
|
|
struct Args {
|
|
|
|
#[arg(short, long)]
|
|
|
|
name: PathBuf,
|
|
|
|
#[arg(short, long)]
|
|
|
|
url: String,
|
|
|
|
#[arg(short, long)]
|
|
|
|
@ -44,7 +55,29 @@ async fn main() -> anyhow::Result<()> { |
|
|
|
let args = Args::parse();
|
|
|
|
log!(Level::Debug, "Arguments: {:?}", args);
|
|
|
|
|
|
|
|
log!(Level::Info, "Creating an HTTP client with Tower layers...");
|
|
|
|
let name = args.name.as_path();
|
|
|
|
|
|
|
|
if name.file_name() != Some(OsStr::new(name)) {
|
|
|
|
return Err(anyhow!("Name must not contain a path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
if name.extension() != Some(OsStr::new("mp4")) {
|
|
|
|
return Err(anyhow!("Only filenames with .mp4 extension are allowed"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let basename = name.file_stem()
|
|
|
|
. ok_or(anyhow!("No valid filename given"))?;
|
|
|
|
|
|
|
|
if let Err(error) = create_dir(basename).await {
|
|
|
|
if ErrorKind::AlreadyExists == error.kind() {
|
|
|
|
if ! Path::new(basename).is_dir() {
|
|
|
|
return Err(anyhow!("Unable to create directory at {:?}", basename));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(anyhow!("Unable to create directory at {:?}", basename));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
set_current_dir(basename)?;
|
|
|
|
|
|
|
|
let concurrency = args.concurrency.unwrap_or(10);
|
|
|
|
let timeout = args.timeout.unwrap_or(10);
|
|
|
|
@ -53,6 +86,9 @@ async fn main() -> anyhow::Result<()> { |
|
|
|
let m3u8_uri = Uri::try_from(&args.url)?;
|
|
|
|
let m3u8_path_and_query = m3u8_uri.path_and_query()
|
|
|
|
. ok_or(anyhow!("Problem path and query in m3u8 uri"))?;
|
|
|
|
|
|
|
|
log!(Level::Info, "Creating an HTTP client with Tower layers...");
|
|
|
|
|
|
|
|
let mut state = client::State::new(&m3u8_uri, concurrency, timeout)?;
|
|
|
|
|
|
|
|
log!(Level::Info, "Get segments...");
|
|
|
|
@ -91,5 +127,45 @@ async fn main() -> anyhow::Result<()> { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let outfile = current_dir()?;
|
|
|
|
let outfile = outfile.parent().ok_or(anyhow!("Can't get output dir"))?;
|
|
|
|
let mut outfile = outfile.canonicalize()?;
|
|
|
|
outfile.push(name);
|
|
|
|
let outfile = outfile.as_os_str();
|
|
|
|
|
|
|
|
let ffmpeg = which("ffmpeg")?;
|
|
|
|
let ffmpeg = ffmpeg.as_path().as_os_str();
|
|
|
|
|
|
|
|
let index = Path::new(m3u8_uri.path()).file_name()
|
|
|
|
. ok_or(anyhow!("unable to get index filename from url"))?;
|
|
|
|
let index = Path::new(index).canonicalize()?;
|
|
|
|
let index = index.as_os_str();
|
|
|
|
|
|
|
|
log!(Level::Debug, "ffmpeg: {:?}", ffmpeg);
|
|
|
|
log!(Level::Debug, "index: {:?}", index);
|
|
|
|
log!(Level::Debug, "outfile: {:?}", outfile);
|
|
|
|
|
|
|
|
log!( Level::Info
|
|
|
|
, "execute: {} -allowed_extensions ALL -i {} -c copy {}"
|
|
|
|
, escape(ffmpeg.try_into()?)
|
|
|
|
, escape(index.try_into()?)
|
|
|
|
, escape(outfile.try_into()?) );
|
|
|
|
let mut child = Command::new(ffmpeg)
|
|
|
|
. arg("-allowed_extensions")
|
|
|
|
. arg("ALL")
|
|
|
|
. arg("-i")
|
|
|
|
. arg(index)
|
|
|
|
. arg("-c")
|
|
|
|
. arg("copy")
|
|
|
|
. arg(outfile)
|
|
|
|
. spawn()?;
|
|
|
|
|
|
|
|
let status = child.wait()?;
|
|
|
|
|
|
|
|
log!(Level::Info, "ffmpeg status: {}", status);
|
|
|
|
|
|
|
|
set_current_dir("..")?;
|
|
|
|
remove_dir_all(basename).await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|