use std::process::Command; use std::fs::File; use std::io::{self, BufReader, Read, Write}; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use json::{self, JsonValue}; use reqwest::{Response, StatusCode}; use reqwest::header::Charset; use reqwest::header::{ContentDisposition, ContentType, DispositionParam, DispositionType}; use zip::ZipArchive; use {construct_cookie, read_response_body, try_to_get_response, try_to_get_valid_response}; use DownloadError; #[derive(Debug)] pub enum Presentation { PublishToGo { name: String, download_url: String, }, VideoOnDemand { name: String, resource_id: String, query_string: String, }, } impl Presentation { pub(crate) fn name(&self) -> &str { match *self { Presentation::PublishToGo { ref name, .. } | Presentation::VideoOnDemand { ref name, .. } => name, } } pub(crate) fn download(&self, out_dir: &Path) -> Result<(), DownloadError> { fn download_to_file_and_check bool>( response: &mut Response, out_file: &Path, check: F, ) -> Result<(), io::Error> { println!("Output file: {}", out_file.display()); if out_file.exists() { println!("File is present already. Checking validity..."); if check(out_file) { println!("Valid file! Skipping!"); return Ok(()); } else { println!("Corrupt file! Starting anew!"); } // TODO?: We could try resuming the download if the server supports it // http://stackoverflow.com/questions/41444297/ // http://stackoverflow.com/questions/3428102/ } download_to_file(response, out_file)?; assert!(check(out_file)); Ok(()) } match *self { Presentation::PublishToGo { ref download_url, .. } => { let response_res = try_to_get_valid_response( |client| { let mut request_builder = client.get(download_url); request_builder.header(construct_cookie()); request_builder }, |resp| resp.headers().get::().is_some(), ); if response_res.is_err() { // Appears to be the reason... return Err(DownloadError::AuthorizationTimeout); } let mut response = response_res.unwrap(); let filename = { let content_disposition = response.headers().get::().unwrap(); assert_eq!(content_disposition.disposition, DispositionType::Attachment); let mut name = None; for param in &content_disposition.parameters { match *param { DispositionParam::Filename(ref charset, _, ref vec) => { assert_eq!(&Charset::Ext("UTF-8".to_string()), charset); name = Some( String::from_utf8(vec.to_vec()) .expect("Suggested filename isn't valid UTF-8!"), ); } _ => continue, } } name.expect("Missing filename in ContentDisposition!") }; let mut path = PathBuf::from(out_dir); path.push(fix_filename(&filename)); download_to_file_and_check(&mut response, &path, |path| check_zip(path)) .map_err(DownloadError::IoError) } Presentation::VideoOnDemand { ref resource_id, ref query_string, .. } => { let videos = { let payload = format!( "{{\"getPlayerOptionsRequest\":{{\ \"ResourceId\":\"{}\",\ \"QueryString\":\"{}\"\ }}}}", resource_id, query_string ); let url = "https://streams.tum.de/Mediasite/PlayerService/\ PlayerService.svc/json/GetPlayerOptions"; let mut res = try_to_get_response(|client| { let mut request_builder = client.post(url); request_builder .header(construct_cookie()) .header(ContentType::json()) .body(payload.to_string()); request_builder }); let json_text = read_response_body(&mut res); if json_text.contains("You are not authorized to view the requested content") { return Err(DownloadError::AuthorizationTimeout); } let json = json::parse(&json_text).expect("Failed parsing the json!"); let mut vec = Vec::new(); for stream in json["d"]["Presentation"]["Streams"].members() { assert_eq!(stream["VideoUrls"].len(), 1); vec.push(stream["VideoUrls"][0]["Location"].to_string()); } vec }; for (i, download_url) in videos.iter().enumerate() { let mut response = try_to_get_valid_response( |client| { let mut request_builder = client.get(download_url); request_builder.header(construct_cookie()); request_builder }, |resp| resp.status() == StatusCode::Ok, ).unwrap(); // TODO: Support formats besides mp4 // extract extension from url or somewehre else... let filename = format!("{} - {}.mp4", fix_filename(self.name()), i + 1); let mut path = PathBuf::from(out_dir); path.push(fix_filename(&filename)); let mut path = PathBuf::from(out_dir); path.push(fix_filename(&filename)); download_to_file_and_check(&mut response, &path, |path| check_video(path)) .map_err(DownloadError::IoError)?; } Ok(()) } } } } impl<'a> From<&'a JsonValue> for Presentation { fn from(json: &'a JsonValue) -> Presentation { if json["DownloadUrls"].len() >= 1 { assert_eq!(json["DownloadUrls"].len(), 1); Presentation::PublishToGo { name: json["Name"] .clone() .take_string() .expect("json: Presentation didn't contain a \"Name\"!"), download_url: json["DownloadUrls"][0]["Url"] .clone() .take_string() .expect("json: Presentation didn't have a download url!"), } } else { // No "Publish To Go" -> download video streams manually let player_url = json["PlayerUrl"].to_string(); let index = player_url.find("?catalog=").unwrap(); let index_end_query = index + "?catalog=499c5d25-37a6-4853-95d5-bfbf7f76fcf2".len(); let index_start_resource = index - "cdf23465581b45aa92d588ebd59619d91d".len(); Presentation::VideoOnDemand { name: json["Name"] .clone() .take_string() .expect("json: Presentation didn't contain a \"Name\"!"), query_string: player_url[index..index_end_query].to_string(), resource_id: player_url[index_start_resource..index].to_string(), } } } } fn download_to_file(response: &mut Response, path: &Path) -> Result<(), io::Error> { let mut file = File::create(&path).expect("Failed to create file!"); let mut reader = BufReader::new(response); let mut buf = [0u8; 8 * 1024]; let update_duration_millis = 1000; let mut last = Instant::now(); let mut done = 0; loop { let len = match reader.read(&mut buf) { Ok(0) => break, Ok(len) => len, Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, Err(e) => return Err(e), }; file.write_all(&buf[0..len]) .expect("Failed writing into the file!"); done += len; let now = Instant::now(); if now - last > Duration::from_millis(update_duration_millis) { print!( "{:.*} kB/s \r", 2, done as f64 * 1000.0 / update_duration_millis as f64 / 1024.0 ); ::std::io::stdout() .flush() .expect("Failed flushing the terminal!"); last = now; done = 0; } } println!("Download successful!"); Ok(()) } fn fix_filename(string: &str) -> String { string .replace('/', "_") .replace('\\', "_") .replace(':', " -") .replace('*', "%2A") .replace('?', "%3F") .replace('"', "'") .replace('<', "'") .replace('>', "'") .replace('|', "_") } fn check_zip(path: &Path) -> bool { assert!(path.exists()); assert!(path.is_file()); let file = File::open(path).expect("Failed opening the zip file!"); ZipArchive::new(file).is_ok() } fn check_video(path: &Path) -> bool { let command = Command::new("ffmpeg") .arg("-v") .arg("error") .arg("-i") .arg(path) .arg("-f") .arg("null") .arg("-") .output(); match command { Ok(output) => output.stderr.is_empty(), Err(_) => { println!( "Failed to check {} with ffmpeg. Perhaps ffmpeg isn't in your path. \ Skipping check...", path.display() ); true } } }