commit
29eb221469
4 changed files with 2079 additions and 0 deletions
-
1.gitignore
-
1919Cargo.lock
-
18Cargo.toml
-
141src/main.rs
@ -0,0 +1 @@ |
|||||
|
**/target |
||||
1919
Cargo.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,18 @@ |
|||||
|
[package] |
||||
|
name = "hlsclient" |
||||
|
version = "0.1.0" |
||||
|
edition = "2021" |
||||
|
|
||||
|
[dependencies] |
||||
|
anyhow = "1.0" |
||||
|
clap = { version = "4.5", features = [ "derive" ] } |
||||
|
env_logger = "0.11" |
||||
|
futures-util = "0.3" |
||||
|
http = "1.2" |
||||
|
log = "0.4" |
||||
|
m3u8-rs = "6.0" |
||||
|
reqwest = "0.12" |
||||
|
tokio = { version = "1.42", features = [ "macros", "rt-multi-thread" ] } |
||||
|
tower = { version = "0.5", features = [ "buffer", "limit" ] } |
||||
|
tower-http-client = "0.4" |
||||
|
tower-reqwest = "0.4" |
||||
@ -0,0 +1,141 @@ |
|||||
|
use std::time::Duration;
|
||||
|
|
||||
|
use anyhow::anyhow;
|
||||
|
use clap::Parser;
|
||||
|
use env_logger::Env;
|
||||
|
use http::{uri::{Authority, Scheme}, Request, Response, Uri};
|
||||
|
use m3u8_rs::Playlist;
|
||||
|
use reqwest::Body;
|
||||
|
use tower::{ServiceBuilder, ServiceExt as _};
|
||||
|
use tower_http_client::{client::BodyReader, ServiceExt as _};
|
||||
|
use tower_reqwest::HttpClientLayer;
|
||||
|
|
||||
|
use log::{log, Level};
|
||||
|
|
||||
|
|
||||
|
#[allow(dead_code)]
|
||||
|
#[derive(Debug)]
|
||||
|
struct DownloadMessage {
|
||||
|
url: String,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Debug, Parser)]
|
||||
|
struct Args {
|
||||
|
#[arg(short, long)]
|
||||
|
url: String |
||||
|
}
|
||||
|
|
||||
|
type HttpClient = tower::util::BoxCloneService<Request<Body>, Response<Body>, anyhow::Error>;
|
||||
|
|
||||
|
#[derive(Clone, Debug)]
|
||||
|
struct State {
|
||||
|
scheme: Scheme,
|
||||
|
auth: Authority,
|
||||
|
client: HttpClient,
|
||||
|
}
|
||||
|
|
||||
|
impl State {
|
||||
|
async fn get(&mut self, path_and_query: &str) -> anyhow::Result<()> {
|
||||
|
let uri = Uri::builder()
|
||||
|
. scheme(self.scheme.clone())
|
||||
|
. authority(self.auth.clone())
|
||||
|
. path_and_query(path_and_query)
|
||||
|
. build()?;
|
||||
|
|
||||
|
let mut response = self
|
||||
|
. client
|
||||
|
. get(uri)
|
||||
|
. send()?
|
||||
|
. await?;
|
||||
|
|
||||
|
anyhow::ensure!(response.status().is_success(), "response failed");
|
||||
|
log!(Level::Debug, "-> Response: {:?}", response);
|
||||
|
|
||||
|
let body = BodyReader::new(response.body_mut()).bytes().await?;
|
||||
|
match m3u8_rs::parse_playlist(&body) {
|
||||
|
Result::Ok((_, Playlist::MasterPlaylist(pl))) =>
|
||||
|
log!(Level::Info, "Master playlist: {:?}", pl),
|
||||
|
Result::Ok((_, Playlist::MediaPlaylist(pl))) =>
|
||||
|
log!(Level::Info, "Media playlist: {:?}", pl),
|
||||
|
Result::Err(e) =>
|
||||
|
Err(anyhow!("Parsing error: {}", e))?,
|
||||
|
};
|
||||
|
|
||||
|
Ok(())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[tokio::main]
|
||||
|
async fn main() -> anyhow::Result<()> {
|
||||
|
eprintln!("-> Initialize env_logger");
|
||||
|
|
||||
|
let env = Env::default()
|
||||
|
. filter_or("LOG_LEVEL", "error")
|
||||
|
. write_style_or("LOG_STYLE", "always");
|
||||
|
env_logger::init_from_env(env);
|
||||
|
|
||||
|
log!(Level::Info, "-> Parse command line arguments...");
|
||||
|
|
||||
|
let args = Args::parse();
|
||||
|
log!(Level::Debug, "-> Arguments: {:?}", args);
|
||||
|
|
||||
|
|
||||
|
log!(Level::Info, "-> Creating an HTTP client with Tower layers...");
|
||||
|
|
||||
|
let m3u8_uri = Uri::try_from(&args.url)?;
|
||||
|
let m3u8_scheme = m3u8_uri.scheme()
|
||||
|
. ok_or(anyhow!("Problem scheme in m3u8 uri"))?;
|
||||
|
let m3u8_auth = m3u8_uri.authority()
|
||||
|
. ok_or(anyhow!("Problem authority in m3u8 uri"))?;
|
||||
|
let m3u8_path_and_query = m3u8_uri.path_and_query()
|
||||
|
. ok_or(anyhow!("Problem path and query in m3u8 uri"))?;
|
||||
|
let state = State {
|
||||
|
scheme: m3u8_scheme.clone(),
|
||||
|
auth: m3u8_auth.clone(),
|
||||
|
client: ServiceBuilder::new()
|
||||
|
// Add some layers.
|
||||
|
. buffer(64)
|
||||
|
. rate_limit(10, Duration::from_secs(1))
|
||||
|
. concurrency_limit(50)
|
||||
|
// Make client compatible with the `tower-http` layers.
|
||||
|
. layer(HttpClientLayer)
|
||||
|
. service(reqwest::Client::new())
|
||||
|
. map_err(anyhow::Error::msg)
|
||||
|
. boxed_clone(),
|
||||
|
};
|
||||
|
log!(Level::Debug, "-> state: {:?}", state);
|
||||
|
|
||||
|
// I think about a worker pool... probably of concurrency_limit amount.
|
||||
|
// The worker needs to get the url. Our first target is to store the
|
||||
|
// data on a file with the same name as the last part of the URL.
|
||||
|
|
||||
|
log!(Level::Info, "-> Sending concurrent requests...");
|
||||
|
|
||||
|
let tasks = (0..1).map({
|
||||
|
|i| {
|
||||
|
let state = state.clone();
|
||||
|
let m3u8_path_and_query = m3u8_path_and_query.clone();
|
||||
|
tokio::spawn(async move {
|
||||
|
let mut state = state.clone();
|
||||
|
state.get(m3u8_path_and_query.as_str()).await?;
|
||||
|
log!( Level::Info
|
||||
|
, "[task {}]: {} completed successfully!"
|
||||
|
, i
|
||||
|
, m3u8_path_and_query );
|
||||
|
|
||||
|
anyhow::Ok(())
|
||||
|
})
|
||||
|
}
|
||||
|
});
|
||||
|
|
||||
|
let results = futures_util::future::join_all(tasks).await;
|
||||
|
for result in &results {
|
||||
|
match result {
|
||||
|
Err(e) => log!(Level::Error, "{}", e),
|
||||
|
Ok(Err(e)) => log!(Level::Error, "{}", e),
|
||||
|
_ => (),
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
Ok(())
|
||||
|
}
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue