Browse Source

Add partial content support

main
Georg Hopp 11 months ago
parent
commit
8d62143bd4
Signed by: ghopp GPG Key ID: 4C5D226768784538
  1. 55
      src/client.rs

55
src/client.rs

@ -1,12 +1,12 @@
use std::{path::Path, time::Duration};
use std::{io::ErrorKind, path::Path, time::Duration};
use anyhow::anyhow;
use futures_util::StreamExt as _;
use http::{header::{CONTENT_TYPE, RANGE}, uri::{Authority, Scheme}, Request, Response, Uri, request::Builder as RequestBuilder};
use http::{header::{CONTENT_TYPE, RANGE}, request::Builder as RequestBuilder, uri::{Authority, Scheme}, Request, Response, StatusCode, Uri};
use http_body_util::BodyDataStream;
use m3u8_rs::{MediaPlaylist, MediaSegment, Playlist};
use reqwest::{redirect::Policy, Body};
use tokio::{fs::File, io::AsyncWriteExt as _, time::timeout};
use tokio::{fs::{symlink_metadata, File}, io::AsyncWriteExt as _, time::timeout};
use tower::{ServiceBuilder, ServiceExt as _};
use tower_http_client::{client::BodyReader, ServiceExt as _};
use tower_reqwest::HttpClientLayer;
@ -96,7 +96,22 @@ impl State {
pub(super) async fn get_m3u8_segment(&mut self, uri: &Uri)
-> Result<(), DownloadError>
{
let mut response = self.request(uri, 0).await
// I consider a missing path as fatal... there is absolutely nothing we can do about it
// and we need all files from the playlist.
let filename = Path::new(uri.path())
. file_name()
. expect("no filename in path_and_query");
let metadata = match symlink_metadata(filename).await {
Ok(metadata) => Some(metadata),
Err(error) => match error.kind() {
ErrorKind::PermissionDenied => panic!("Permission denied on: {:?}", filename),
_ => None,
}
};
let mut response = self.request( uri
, metadata.map_or(0, |m| m.len()) )
. await
. map_err(|e| DownloadError::new(uri.clone(), Some(e)))?;
// We always need the content-type to be able to decide
@ -110,14 +125,23 @@ impl State {
, Some(anyhow!(message)) ));
}
// I consider a missing path as fatal... there is absolutely nothing we can do about it
// and we need all files from the playlist.
let path = uri.path();
let filename = Path::new(path)
. file_name()
. expect("no filename in path_and_query");
let mut file = File::create(filename).await
. expect("can not create file for writing");
let mut file = match response.status() {
StatusCode::PARTIAL_CONTENT =>
// Here we assume that this response only comes if the requested
// range was fullfillable and thus is the data range in the
// response. Thats why I do not check the content-range header.
// If that assumption does not hold this needs to be fixec.
File::options()
. create(true)
. append(true)
. open(filename)
. await
. expect("can not create file for writing"),
_ =>
File::create(filename).await
. expect("can not create file for writing"),
};
// read body into file as stream
let mut body_stream = BodyDataStream::new(response.body_mut());
@ -133,6 +157,9 @@ impl State {
file.write_all(data.as_ref()).await
. map_err(|e|
DownloadError::new(uri.clone(), Some(e.into())) )?;
file.flush().await
. map_err(|e|
DownloadError::new(uri.clone(), Some(e.into())) )?;
},
}
};
@ -179,11 +206,11 @@ impl State {
Ok(filename.to_string())
}
async fn request(&mut self, uri: &Uri, from: usize) -> anyhow::Result<Response<Body>>
async fn request(&mut self, uri: &Uri, from: u64) -> anyhow::Result<Response<Body>>
{
let request = RequestBuilder::new()
. uri(uri)
. header(RANGE, format!("{}-", from))
. header(RANGE, format!("bytes={}-", from))
. body(Body::default())?;
log!(Level::Debug, "{:?}", request);

Loading…
Cancel
Save